Skip to main content
Prerequisite: Install Docker and followed the Development setup guide.

CI image pulls

Contract startup and type generation retry registry throttling and transient image-pull transport failures up to seven attempts, waiting 10/20/40/80/120/120 seconds plus 0-25% jitter. SQL tests, resets and pushes are never replayed. Authentication, missing-image and schema failures fail immediately; exhausted pulls remain failures. Type generation retains bounded private diagnostics and publishes only successful output. The E2E baseline keeps its existing service set, excluding only Edge Runtime and Functions. SQL-only contracts keep their existing narrower service sets. CI uses public.ecr.aws; no image archive cache is introduced. Regression coverage: TMPDIR=/private/tmp node --test apps/database/scripts/run-supabase-image-retry.test.js. Real cold/warm/corrupt baseline and contract acceptance runs in exact-commit CI.

Independent SQL session fixtures

The pgTAP concurrency fixtures include tests/helpers/local-dblink.inc before starting their transactions or writing fixture data. On a stock supabase start stack, the helper connects through db.supabase.internal:5432 with the CLI’s local development credentials. No extra environment is required. PostgreSQL trusts loopback connections inside the container, so supplying a password to 127.0.0.1 still does not authenticate dblink for a non-superuser role. For a different disposable test database, configure the existing PostgreSQL session setting test.dblink_connstr with a password-authenticated connection to that same database (for example, via PGOPTIONS in a direct pg_prove runner). Do not use production credentials or point independent sessions at another instance. The local default has a three-second connection timeout; custom connection strings should also set a bounded connect_timeout. The helper probes and closes a connection before any fixtures run. If connection setup fails, the file emits a TAP skip with setup guidance and exits successfully instead of aborting halfway through committed fixture writes. Connection strings and server errors are not printed. A skip means concurrency was not verified; acceptance on the stock stack requires actual passing assertions. Subsequent SQL and assertion errors remain failures. Destroy the owned disposable stack and its volumes after running these fixtures, since some commit across sessions. Regression checks: run the five dblink fixtures with the default connection, then with test.dblink_connstr targeting an unavailable port and confirm each reports a clean skip before fixture setup.

CLI upgrade type drift

Regenerate packages/types/src/supabase.ts from the complete applied migration snapshot with the upgraded workspace CLI, using public,private,storage, then format with the pinned Oxc version. The isolated contract lifecycle uses those same schemas. Do not regenerate only the first failing table or hand-edit the generated declarations. To distinguish generator behavior from database changes, run both CLI generators against the same disposable database. The 2.117.0 to 2.121.0 upgrade changes postgres-meta from v0.99.0 to v0.100.0: the newer generator adds ComputedFields and computed row fields, excludes generated columns from writes, narrows non-null JSON columns, and emits Record<PropertyKey, never> for no-argument RPCs. The same-database comparison also distinguishes these changes from the newer service baseline’s Storage lifecycle columns and version-aware function signatures. Check every active workspace after regeneration: Turbo can stop at the first failure. Use queued per-workspace type checks; when cold dist declarations are missing, use a temporary no-emit config mapping package exports to source while preserving the workspace’s file exclusions. Do not build locally to fill dist. Narrow object-only JSON producers and validated write inputs to NonNullable<Json>; keep genuinely nullable columns and sanitized delivery values nullable. Include computed relationships in row fixtures, but never insert computed fields. Run focused tests and require the exact pushed commit’s TypeScript CI to pass. The Connected Mail drift check compares three private tables with the committed types. Their upgrade delta is ComputedFields: never; their row columns, optionality and relationships are unchanged. The new generator also quotes property names, which Oxc may unquote. Normalize these equivalent AST property keys without discarding actual contract differences. Regression coverage is apps/database/scripts/connected-mail-types-drift.test.mjs, including rejection of semantic changes in quoted output. On a disposable Linux devbox, keep Bun and the clone local to the task. Docker 29 may require command-scoped DOCKER_API_VERSION=1.41; the CLI’s volume prune can then fail because --all requires API 1.42. Verify the owned project has no containers, remove only its exact named volume and network, and verify absence before removing its retained disposable directory. Never run a global prune.

Basic Commands

Starting Supabase

Local ports are pinned in apps/database/supabase/config.toml:
Run bun sb:status at any time to print the live URLs, keys, and the DB connection string for your running instance. Prefer reading these values from bun sb:status over hard-coding them, since they are the source of truth for the current container.

Stopping Supabase

To stop the local Supabase instance:

Checking Status

To view the current URLs and status of your local Supabase instance:

Development Workflow

When developing for Tuturuuu, you have several options to start your environment:
  1. Standard Approach: Start Next.js apps and Supabase separately.
  2. Enhanced Development Experience: Use the devx command for a streamlined setup.
    This command:
    • Stops any running Supabase instance and saves current data as backup
    • Installs all dependencies
    • Starts a new Supabase instance (using backed up data)
    • Starts all Next.js apps in development mode
  3. Fresh Database Setup: When switching branches with potential schema changes.
    This command (bun sb:stop && bun sb:start && bun sb:reset && bun dev):
    • Stops any running Supabase instance (without backup)
    • Starts a new Supabase instance
    • Resets the database to use the latest schema and seed data
    • Starts all Next.js apps in development mode
    App-scoped variants also exist (for example bun devrs:web, bun devrs:tasks) that reset Supabase and then start only that app’s dev server.
Use bun devrs when switching between branches that might have different database migrations to ensure your local database schema matches the branch you’re working on.

Syncing With Schema Changes

If you’re keeping your Next.js server running but need to reset your database to match the current branch’s schema:
This command will reset your local database to use the latest schema definitions and automatically regenerate TypeScript types. After the reset, the database package also runs the local AI credits bootstrap, which syncs gateway models through the local Supabase REST API and retries the short PostgREST schema-cache warmup window that can happen immediately after containers restart.

Database Schema Management

Making Schema Changes

There are two approaches to modifying the database schema:

1. Using the Supabase UI

  1. Navigate to your local Supabase Studio at http://localhost:8003
  2. Make your changes through the UI
  3. Generate a migration file:
    This creates a migration file based on the differences between your current schema and the previous state.
Be cautious when using sb:diff for schema changes. If you rename columns or tables, the migration will drop the old ones and create new ones, which can result in data loss in production environments.

2. Creating Manual Migrations

For more control, you can create empty migration files and populate them manually:
This creates a new empty migration file in apps/database/supabase/migrations using the provided migration name. If you omit the name, the helper uses new_migration.

Applying Migrations

After creating a migration, apply it to your local database:
This same process is used to keep our production database up-to-date with the schema defined in the production branch.

Validating an Exact Worktree in Isolation

Database plans pinned to an older commit must not reset or reuse the ordinary tuturuuu stack when its migration history belongs to another checkout. Run the disposable validator from the repository root instead:
For a focused pgTAP file, pass its path relative to apps/database:
To replace the checked-in database types only after the disposable reset and focused pgTAP test both succeed, add the exact approved output path:
The type generator uses the same pinned Supabase binary and disposable project, includes the public, private, and storage schemas, buffers its output, and atomically replaces packages/types/src/supabase.ts only after a successful, non-empty result. Reset, pgTAP, type generation, or output-write failure keeps the existing generated file unchanged. No other output path is accepted. The runner derives a Docker-safe identity from the exact worktree path and commit, selects an available deterministic eight-port block, and prints both. It copies only tracked apps/database/supabase files into an owned tuturuuu-supabase-* temporary directory, changing its project id and ports without editing tracked config. Staging failure removes only its partial project and preserves the original error. Secondary cleanup failures emit only Secondary staging cleanup failure., without paths or exception details. node --test scripts/run-supabase-isolated-staging-cleanup.test.mjs checks error identity, retained partial files and scoped removal without Docker. The lifecycle starts the copied project, resets its exact-base migrations, runs full or focused pgTAP, and stops only that project with --no-backup. It never uses supabase stop --all or the ordinary sb:stop/sb:reset aliases. SIGINT and SIGTERM request scoped cleanup while preserving the command or signal exit code. Hard termination can retain .tuturuuu-isolated-supabase.json, recording the project id, worktree commit, ports and last complete lifecycle step. Resume or clean the recorded directory without guessing its Docker identity:
Use the printed disposable path; its temporary parent varies by operating system. Cleanup refuses paths outside the owned pattern; failed Docker cleanup retains metadata for a safe retry. Recovery metadata updates atomically replace the prior regular file with mode 0600; temporary creation stays exclusive and symlink targets are rejected. Fault tests interrupt descriptor writes, then verify partial bytes and the prior record. Run the retained proposal’s suite separately from the repository root:
This .node-test.mjs suite is absent from root script discovery and active CI; apps/database/scripts/*.test.* results do not include it.

Generating TypeScript Types

After schema changes, regenerate the TypeScript types to keep your code in sync with the database schema:
The generator includes the public, private, and storage schemas. Keep server-owned/private tables out of Supabase REST exposure, but still regenerate types after migrations so server-only helpers and storage paths can import accurate generated row shapes.
This step is automatically performed when running bun sb:reset, making it useful when catching up with a new branch’s schema.

Using Generated TypeScript Types

The Supabase-generated TypeScript types are available at packages/types/src/supabase.ts. These types are accessible to all apps that have the @tuturuuu/types package installed. You can use these types to ensure type safety when working with Supabase data: With supabase-js v2, table types flow from the client itself rather than a generic on .from(). The Tuturuuu Supabase helpers in @tuturuuu/supabase are already typed with the generated Database type, so queries are type-safe out of the box. Reach for the Database type directly when you need a specific row shape:

Short-hand Type Access

For more convenient access to common table types, Tuturuuu also provides short-hand type definitions in packages/types/src/db.ts. These are easier to use and remember than the full database type paths:
You can add your own short-hand types to db.ts for tables you frequently work with. This is especially useful for tables that have complex structures or need additional client-side properties. This ensures that your code correctly interacts with the database schema, reducing runtime errors and improving development experience.

Migration Files

All migration files are stored in apps/database/supabase/migrations. These files:
  • Contain SQL commands that create and modify the database schema
  • Are executed in order based on the timestamp prefix in their filenames
  • Include descriptive names after the timestamp to help developers understand their purpose
When contributing new migrations in a Pull Request, always add them after the latest migration file from the main branch. This maintains the correct execution order and prevents issues when syncing the production database.

Local Authentication

A local mail server (InBucket) is automatically set up by Supabase to handle authentication emails. You can access it at http://localhost:8004. With InBucket, you can:
  • Receive all authentication emails sent by your local Supabase instance
  • Test any email combination without needing actual mail delivery
  • View password reset links, confirmation emails, and other authentication flows
  • Troubleshoot email templates and content
This makes it easy to test different authentication scenarios without configuring a real email service or waiting for actual email delivery. Five seed accounts are pre-configured for local development:
  1. local@tuturuuu.com
  2. user1@tuturuuu.com
  3. user2@tuturuuu.com
  4. user3@tuturuuu.com
  5. user4@tuturuuu.com
These accounts are already set up with the necessary data, allowing you to quickly test the app’s functionality. However, you can register any new email address and the authentication emails will be captured by InBucket for you to inspect.
OTP send limits include email-scoped cooldowns in the web process. Playwright tests that only need to assert the OTP stage should use a dedicated throwaway email address instead of a seed login account. Reserve seed accounts for tests that must complete authentication.

Playwright E2E Safety

Web E2E runs must use the local Supabase stack. For local debugging and patch iteration, run the web app and Playwright on the native machine so code changes are picked up quickly and server logs stay visible. Avoid bun test:e2e, bun --cwd apps/web test:e2e, and bun test:e2e:web:docker as the first local debugging path. Those commands route through scripts/run-web-e2e-docker.js, which writes tmp/e2e/web.env, resets local Supabase, builds and runs the production-style Docker web stack, and then runs Playwright. Reserve that Dockerized path for explicit CI-parity checks or production-Docker runtime bugs. For the native workflow, start or reset local Supabase with the database package scripts, start apps/web natively with the local E2E environment, and run the focused Playwright spec directly from apps/web. Native web server Supabase URLs should resolve to http://127.0.0.1:8001; host.docker.internal is only for the Dockerized web stack. Do not point E2E at a cloud Supabase project. The Playwright global setup fails fast if BASE_URL is not local or if NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVER_URL, or SUPABASE_URL resolves outside the local Supabase origins on port 8001.

Further Information

For more details about Supabase CLI usage, refer to the Supabase CLI documentation.

Row Level Security (RLS)

Row Level Security (RLS) is a powerful Postgres feature that allows you to control access to rows in a database table based on the user making the request. In Tuturuuu, we use RLS extensively to ensure data security.

Enabling RLS

RLS should be enabled on all tables in exposed schemas (like public). When creating tables through the Supabase UI, RLS is enabled by default. For tables created using SQL, you need to explicitly enable RLS:

Creating RLS Policies

Policies define the conditions under which users can access or modify data. Here are some common patterns used in Tuturuuu:

Organization-based Access

In Tuturuuu, workspace-scoped resource tables use ws_id, and membership rows use workspace_members.ws_id plus the workspace_member_type enum. Use MEMBER when an example requires a full member rather than a guest. For application-specific mutations, pair membership with the maintained permission helper and confirm the permission name in the generated enum. The canonical RLS policy reference explains when a different membership or permission rule is appropriate.

Role-based Access

Use public.has_workspace_permission(p_ws_id, p_user_id, p_permission) rather than joining retired member-role columns. Permission checks should still be paired with membership when a workspace-wide default permission must not admit non-members. If a policy needs a recursion-free membership lookup, keep the helper in a non-exposed schema, derive identity from the request, and harden its execution boundary before referencing it from a policy:
Because private is not a Data API schema, this narrowly granted helper cannot be invoked as an exposed RPC. A public Data API RPC needs its own internal workspace permission check; a page or route check is not enough. Revoke public and anon, grant only the required caller role, and cover the grant matrix and allowed/denied calls with pgTAP. See Workspace-Scoped Security Definer RPCs.

Performance Optimization for RLS

For better performance in your RLS policies:
  1. Wrap function calls in subqueries:
  2. Use hardened security-definer helpers only when needed. The private.can_access_workspace recipe above shows the required caller, service-role, search-path, and grant controls. Wrap stable helper calls in a scalar subquery so Postgres can evaluate them once per statement:
  3. Add explicit filters in your queries even when you have RLS:

Testing RLS Policies

Tuturuuu ships a large suite of database tests in apps/database/supabase/tests/. These are pgTAP test files (most wrap their assertions in a transaction and roll back), and they run against the local Supabase instance through the Supabase CLI. To run the database test suite locally:
  1. Make sure a local Supabase instance is running (bun sb:start).
  2. Add or edit a *.sql test file in apps/database/supabase/tests/.
  3. Run the tests through the database workspace’s Supabase CLI wrapper:
    run-supabase.js resolves the pinned local Supabase binary and invokes supabase test db, which executes every tests/*.sql file against the local database. There is no dedicated bun sb:test alias — the test runner is the Supabase CLI itself.
Concurrency fixtures can commit data in independent connections. Reset the isolated database before repeating the full suite; assertions about provenance must filter by the fixture actor or target rather than count unrelated committed rows. The calendar and employee concurrency tests accept the database setting test.dblink_connstr. On local images where postgres is not a superuser, set it after reset, using the disposable database administrator, to a password-authenticated connection to that same database. A Docker-network address uses SCRAM; loopback trust does not satisfy dblink’s password requirement even when a password is supplied. Supply only disposable local credentials, never a linked project DSN. Stop the uniquely named project with supabase stop --no-backup after validation. When updating security tests, distinguish restrictive MFA policies from permissive access grants. Preserve denied-role assertions and seed real Auth identities for verified staff fixtures. A runtime contract rejecting its own migrated function metadata is a product regression, not a reason to relax the test. Each test file begins by enabling pgTAP, then uses helpers like plan(), is(), and throws_ok() to assert behavior. A trimmed example:
Look through the existing files in apps/database/supabase/tests/ (for example user-profile-rls.sql and the private-schema-* suites) to follow the established pgTAP patterns for impersonating roles and asserting RLS behavior.

Database Triggers

Triggers in Postgres allow you to automatically execute a function when a specified database event occurs (INSERT, UPDATE, DELETE). In Tuturuuu, we use triggers for various purposes like:
  • Maintaining audit logs
  • Syncing data between tables
  • Enforcing complex business rules

Creating Triggers

Here’s how to create a trigger in your Tuturuuu development workflow:
  1. First, create a trigger function:
  1. Then, create the trigger:

Common Triggers in Tuturuuu

Audit Logging

Automated Timestamps

Testing Triggers

You can test triggers by running SQL commands in the local Supabase instance and verifying the results:

Seeding Your Database

Database seeding is the process of populating your database with initial data. In Tuturuuu, we use seeding to:
  1. Create test users and workspaces for local development
  2. Initialize lookup tables with standard values
  3. Ensure a consistent starting point for all developers

Seed Files Location

In Tuturuuu, seed files are stored in apps/database/supabase/seed.sql. This file is automatically executed when you run bun sb:reset or start a fresh Supabase instance.

Replaying migrations with existing storage buckets

An in-place remote supabase db reset --linked preserves managed storage tables while replaying migrations. Bucket creation must tolerate an existing bucket ID. Use ON CONFLICT (id) DO NOTHING for creation defaults; retain intentional upserts that apply later configuration changes. This preserves existing bucket settings and objects without changing fresh-database defaults. node --test apps/database/scripts/storage-bucket-inserts.test.js executes every migration bucket insert in a disposable PGlite database, replays each insert, and verifies that creation-only inserts preserve existing settings. This focused check does not replace full migration replay or authorize a remote reset. The database package’s sbr:reset wrapper deletes buckets before resetting; use a separately approved, explicitly targeted procedure when storage must survive.

Real Examples from Tuturuuu’s Seed File

Let’s look at some real examples from Tuturuuu’s seed.sql file:

1. Authentication Users

The seed file creates five default test users with pre-set passwords:
All seed users have the same password: password123, making it easy to log in for testing.

2. Workspaces

The seed creates several workspaces for testing different scenarios:

3. Workspace Members and Roles

The seed also sets up relationships between users and workspaces with different roles:

4. Workspace Features Configuration

The seed file configures workspace features using secrets:

5. Domain-specific Data

The seed includes domain-specific data for different workspace types. For example, user group metric data:
user_group_metrics.is_weighted = false keeps a metric visible for entry and category views without counting it toward report totals or averages. Metric categories live in user_group_metric_categories and are linked through user_group_metric_category_links. When renaming or recreating these metric tables, keep authenticated CRUD grants and service-role grants in the same migration as the table change. The group indicators dashboard reads the renamed tables through protected API/server paths after workspace permission checks, and missing grants can surface as permission denied for table user_group_metric_categories. When code uses createAdminClient() for these protected reads, name the local client sbAdmin. Do not name a service-role client supabase; reserve that name for request-scoped or user-scoped clients so route authorization is easy to audit.

Creating Seed Data

Here’s how to create and modify seed data:
  1. Edit the apps/database/supabase/seed.sql file
  2. Add SQL statements to insert your data
  3. Run bun sb:reset to apply the seed data
Example seed data format:

Creating a Custom Seed File

Sometimes you might want to create a custom seed file for specific testing scenarios:
  1. Create a new SQL file in the apps/database/supabase directory
  2. Add your custom seed data
  3. Run it with the Supabase CLI:

Exporting Current Data as Seed

You can also export your current database data to use as seed data:
This is helpful when you’ve set up data manually and want to preserve it for future development environments. For Tuturuuu development, we recommend:
  1. Start with a fresh database: bun sb:reset
  2. Make changes through the UI or your app
  3. When you’re satisfied, export the data: node apps/database/scripts/run-supabase.js db dump --data-only > apps/database/supabase/new_seed.sql
  4. Edit the generated SQL to keep only what you need
  5. Update the main seed.sql file with your changes
  6. Test by running bun sb:reset again

AI Integration with Vercel AI SDK

Tuturuuu uses Vercel’s AI SDK for its AI features, utilizing structured data generation capabilities that integrate with Supabase. This section covers how to work with AI features in the development workflow.

Overview of AI SDK in Tuturuuu

The AI SDK standardizes integrating various AI models across supported providers into Tuturuuu applications. It enables structured data generation, tool calling, and streaming responses to create rich AI-powered features. The main libraries used are:
  • ai - Core Vercel AI SDK package
  • @ai-sdk/google - Provider-specific integration for Google models
  • @tuturuuu/supabase - Supabase client with Tuturuuu-specific utilities

Generating Structured Data

Tuturuuu uses the AI SDK’s structured data generation capabilities to create typed responses from AI models. This approach ensures type safety and consistent data structures for features like:
  • Flashcards generation
  • Quiz generation
  • Learning plans
  • Task management

Example: Flashcard Generation

The structured data pattern used in Tuturuuu follows this workflow:
  1. Define a schema using Zod
  2. Connect to Supabase for authentication and workspace validation
  3. Generate structured data using the AI SDK
  4. Stream the response to the client
Here’s an example from Tuturuuu’s codebase:

Available Models

Tuturuuu supports multiple AI models through Vercel AI SDK. You can define which models are available in your application by updating the models.ts file in the packages/ai directory:

Creating Custom Schema Types

To create new structured data types for AI generation, add your schema definition to the packages/ai/src/object/types.ts file:

Integration with Supabase

Tuturuuu’s AI features leverage Supabase for:
  1. Authentication - Validating users before making AI requests
  2. Authorization - Checking workspace permissions via workspace_secrets
  3. Feature Flags - Using workspace_secrets to enable/disable AI features per workspace
  4. Storage - Storing AI-generated content for later use
To enable AI features for a workspace, ensure the appropriate flags are set in the workspace_secrets table:

Testing AI Features Locally

When testing AI features in your local environment:
  1. Ensure you have the required API keys set in your .env.local file:
  2. Verify the workspace has the necessary feature flags enabled in your local database
  3. Use the AI-enabled accounts from the seed data (local@tuturuuu.com) as they often have additional permissions

Error Handling

When integrating AI features, implement proper error handling to account for:
  1. Missing API keys
  2. Model unavailability
  3. Invalid user input
  4. Exceeded token limits
Example error handling pattern used in Tuturuuu:
CI pulls Supabase images anonymously from public.ecr.aws through SUPABASE_INTERNAL_IMAGE_REGISTRY; no registry login or package-read permission is needed. Seven attempts allow at most 487.5 seconds of backoff, excluding command runtime. Throttling (toomanyrequests, Rate exceeded or HTTP 429) and classified transient pull transport errors retry; SQL never replays. The baseline fingerprint includes the registry, so a registry change invalidates existing snapshots once.

Backfills with historical payload violations

A NOT VALID check still rejects updates to historical rows, even when the checked payload does not change. For notification email-copy cleanup, first batch rows that satisfy the exact catalog check expression (SQL NULL also passes). If legacy oversized rows remain, capture pg_get_constraintdef, drop the check, clear only their email copies and restore the same named check NOT VALID in the same transaction. Preserve payload bytes. Keep lock_timeout = '1s'; the constraint drop takes ACCESS EXCLUSIVE until commit, so perform it only after the regular batches finish. With no oversized rows, leave the constraint intact. On a disposable full chain, reproduce SQLSTATE 23514 with the original migration, then verify payload bytes, email removal, byte-identical constraint definition, unchanged name, convalidated = false, and rejection of a new oversized insert. Also compare constraint OIDs on the safe-only path. The batch regression is apps/database/supabase/tests/notification-email-payload-cleanup.sql; run the other notification pgTAP suites after applying the remaining migrations.

Task RPC user-client admission

apps/database/supabase/tests/time-task-rpc-scoping.sql checks caller admission and service-only writer grants. Apply the migration snapshot to an isolated local stack before testing the actual Tasks dashboard and Mira Tasks handlers:
The runner accepts only a disposable local root and loopback API URL. It obtains that stack’s test credentials in memory and passes them only to the test process. The fixture creates disposable Auth users and tasks, invokes the real handlers with a real authenticated Supabase client, checks anonymous and different-user RPC denial through PostgREST, and removes its synthetic team workspace and user. Personal workspace cleanup remains owned by the Auth deletion lifecycle. The integration test skips unless the runner supplies its three TEST_SUPABASE_* variables; the ordinary unit suite does not require a running database.