Local Development Setup
Set up a complete local Supabase development environment with automated scripts, pre-configured test users, multi-tenant organizations, and RLS-scoped sample data.
Overview
The local development environment gives you:
- Full Supabase stack running in Docker (database, auth, API, Studio)
- 5 pre-seeded test users with different roles and org memberships
- 3 organizations with proper RLS isolation
- Sample MCP servers, agent cards, tools, and resources
- One-command startup with
./scripts/start-local-supabase.sh --quick
Time required: ~5 minutes (first run downloads Docker images)
Prerequisites
| Name | Type | Description |
|---|---|---|
Docker Desktop | required | Install from docker.com/get-docker. Allocate at least 4 GB RAM and 2 CPU cores in Docker Settings > Resources. |
Supabase CLI | required |
|
psql | required |
|
Node.js v24+ | required | Install from nodejs.org or |
Quick Start
Recommended: Use the A²D CLI Tool for a simpler workflow. This page documents the underlying scripts that the CLI wraps.
Using A²D CLI (Recommended)
git clone <a2d-repo-url> && cd a2d
git submodule update --init --recursive
./a2d startUsing Direct Scripts (Alternative)
git clone <repo-url> && cd a2d-portal
npm install
chmod +x scripts/*.sh
./scripts/start-local-supabase.sh --quick
cp .env.supabase .env.local
npm run devOpen your browser:
- App: http://localhost:3000
- Supabase Studio: http://127.0.0.1:54323
Log in with alpha-owner@test.local / password123.
Step-by-Step Setup
Step 1: Clone and Install
git clone <repo-url>
cd a2d-portal
npm installStep 2: Make Scripts Executable
chmod +x scripts/*.shYou only need to do this once.
Step 3: Start Local Supabase
./scripts/start-local-supabase.shThe script will:
- Check all prerequisites (Docker, Supabase CLI, psql, Node.js)
- Ask if you want to pull data from a remote environment (optional)
- Run
supabase start(downloads Docker images on first run) - Apply all database migrations automatically
- Seed the database with 5 test users, 3 organizations, and sample assets
- Generate
.env.supabasewith local credentials andA2D_PLATFORM_ADMIN_EMAILS - Optionally start the Next.js dev server
On first run, Docker images download takes 2-5 minutes. Subsequent starts are much faster.
Step 4: Set Up Next.js Environment
If you did not let the script start the dev server, do it manually:
cp .env.supabase .env.local
npm run devStep 5: Verify Everything Works
- Open http://localhost:3000
- Log in with
alpha-owner@test.local/password123 - You should see “Local Test Org Alpha” as your organization
- You should see the “Weather Service” and “Calculator API” MCP servers
Test Users and Credentials
All users share the same password: password123
| Full Name | Primary Org | Role | Notes | |
|---|---|---|---|---|
alpha-owner@test.local | Alpha Owner | Org Alpha | Owner | Created Org Alpha and its assets |
alpha-member@test.local | Alpha Member | Org Alpha | Member | Invited by alpha-owner |
beta-owner@test.local | Beta Owner | Org Beta | Owner | Created Org Beta and its assets |
multi-org@test.local | Multi Org User | Org Alpha (default) | Member (both) | Member of Alpha AND Beta; can switch orgs |
admin@test.local | Platform Admin | Org Admin | Owner | Platform admin via A2D_PLATFORM_ADMIN_EMAILS |
User UUIDs
Use these when debugging queries or inspecting RLS behavior:
| UUID | |
|---|---|
alpha-owner@test.local | aaaa0001-0001-0001-0001-000000000001 |
alpha-member@test.local | aaaa0002-0002-0002-0002-000000000002 |
beta-owner@test.local | bbbb0001-0001-0001-0001-000000000001 |
multi-org@test.local | cccc0001-0001-0001-0001-000000000001 |
admin@test.local | dddd0001-0001-0001-0001-000000000001 |
Organizations
| Organization | UUID | Created By | Members |
|---|---|---|---|
| Local Test Org Alpha | 11111111-1111-1111-1111-111111111111 | alpha-owner | alpha-owner (owner), alpha-member (member), multi-org (member) |
| Local Test Org Beta | 22222222-2222-2222-2222-222222222222 | beta-owner | beta-owner (owner), multi-org (member) |
| Local Test Org Admin | 33333333-3333-3333-3333-333333333333 | admin | admin (owner) |
Membership Breakdown
RLS Visibility Matrix
Row-Level Security is organization-scoped. Each user only sees data belonging to their current_organization_id.
What Each User Can See
| Logged-in User | Current Org | MCP Servers | Agent Cards | Can Switch To |
|---|---|---|---|---|
alpha-owner | Org Alpha | Weather Service, Calculator API | Weather Assistant Agent | — |
alpha-member | Org Alpha | Weather Service, Calculator API | Weather Assistant Agent | — |
beta-owner | Org Beta | Inventory Service | Inventory Manager Agent | — |
multi-org (default) | Org Alpha | Weather Service, Calculator API | Weather Assistant Agent | Org Beta |
multi-org (switched) | Org Beta | Inventory Service | Inventory Manager Agent | Org Alpha |
admin | Org Admin | (none seeded) | (none seeded) | — |
Key RLS Behaviors
- Organization isolation — alpha-owner CANNOT see Org Beta data, and vice versa
- Same-org members see same data — alpha-owner and alpha-member see identical assets
- Owner vs member — Both roles see the same data; owner can manage org settings and invite members
- Multi-org switching — multi-org’s visible data changes when they switch
current_organization_id - Platform admin — admin@test.local gets admin UI access via the
A2D_PLATFORM_ADMIN_EMAILSenv var, not via RLS
Testing RLS Isolation
To verify isolation is working:
- As
alpha-owner— See Weather Service + Calculator API. Do NOT see Inventory Service. - As
beta-owner— See Inventory Service. Do NOT see Weather Service or Calculator API. - As
multi-org— See Alpha data by default. After switching org to Beta, see only Beta data. - As
alpha-member— See exact same data as alpha-owner (same org).
Seeded Test Data
MCP Servers
| Name | Org | Type | Created By |
|---|---|---|---|
| Weather Service (Mock) | Alpha | mock | alpha-owner |
| Calculator API (OpenAPI) | Alpha | openapi | alpha-owner |
| Inventory Service (Mock) | Beta | mock | beta-owner |
MCP Tools
| Tool | Server | Description | Mock Scenario |
|---|---|---|---|
get_weather | Weather Service | Get current weather for a city | San Francisco: 72F, sunny |
get_forecast | Weather Service | Get 5-day forecast for a city | New York: Mon-Tue forecast |
calculate | Calculator API | Evaluate a math expression | 2+2 = 4 |
check_stock | Inventory Service | Check stock level for a product | SKU-001: qty 42, in stock |
Agent Cards
| Name | Org | Type | Skills |
|---|---|---|---|
| Weather Assistant Agent | Alpha | mock | Check Weather, Get Forecast |
| Inventory Manager Agent | Beta | mock | — |
Environment Variables
The start script generates .env.supabase automatically. Copy it to .env.local for Next.js:
cp .env.supabase .env.localGenerated Variables
| Variable | Value | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | http://127.0.0.1:54321 | Supabase API endpoint |
NEXT_PUBLIC_SUPABASE_ANON_KEY | (auto-generated) | Public anon key for client-side |
SUPABASE_SERVICE_ROLE_KEY | (auto-generated) | Service role key (server-side only) |
A2D_PLATFORM_ADMIN_EMAILS | admin@test.local | Platform admin email allowlist |
Additional Variables
These are NOT auto-generated. Add them to .env.local manually if needed:
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_A2L_URL=http://a2l.localhost:3000
NEXT_PUBLIC_A2TF_URL=http://a2tf.localhost:3000
CREDENTIALS_ENCRYPTION_KEY=<run: openssl rand -hex 32>
LOG_LEVEL=debug
# Rate limiting configuration (optional)
MAX_PUBLIC_RATE_LIMIT=1000
PLATFORM_RATE_LIMIT=500The CREDENTIALS_ENCRYPTION_KEY is required if you test LLM configuration features. Generate one with openssl rand -hex 32.
Rate Limiting Configuration
Rate limits are configurable via environment variables. In development mode, rate limiting is automatically disabled to make local testing easier.
Environment Variables:
| Variable | Default | Description |
|---|---|---|
MAX_PUBLIC_RATE_LIMIT | 100 | Global ceiling for public MCP endpoints (/api/platform/[id]/mcp) in requests/minute |
PLATFORM_RATE_LIMIT | 500 | Rate limit for platform MCP endpoint (/api/platform-mcp/mcp) in requests/minute |
Setting Custom Limits:
# Add to .env.local
MAX_PUBLIC_RATE_LIMIT=1000
PLATFORM_RATE_LIMIT=500Per-Organization Overrides:
Public MCP endpoints support per-organization rate limit overrides via the database:
-- Set organization-specific limit
UPDATE organizations
SET max_public_rate_limit = 500
WHERE name = 'Premium Tier';
-- View current org limits
SELECT name, max_public_rate_limit
FROM organizations
WHERE max_public_rate_limit IS NOT NULL;
-- Remove override (use global MAX_PUBLIC_RATE_LIMIT)
UPDATE organizations
SET max_public_rate_limit = NULL
WHERE name = 'Premium Tier';The actual limit applied is: min(org.max_public_rate_limit, MAX_PUBLIC_RATE_LIMIT)
Testing Different Rate Limit Scenarios:
By default, rate limiting is disabled in development. To test rate limits locally:
# Enable rate limiting in development
NODE_ENV=production npm run devThen make rapid requests to test the limits:
# Test public endpoint rate limiting
for i in {1..105}; do
echo "Request $i"
curl -X POST http://localhost:3000/api/platform/test-server/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1.0.0"},"capabilities":{}}}' \
-w "\nStatus: %{http_code}\n"
sleep 0.5
doneExpected behavior:
- Requests 1-100:
Status: 200(within limit) - Requests 101+:
Status: 429(rate limited)
Remember to unset NODE_ENV after testing, or rate limiting will continue to apply during development.
Logging Configuration
Control logging verbosity with the LOG_LEVEL environment variable:
| Level | Description | Use Case |
|---|---|---|
debug | Most verbose - detailed debugging information | Local development, troubleshooting |
info | General informational messages (default) | Production monitoring |
warn | Warning messages for potentially harmful situations | Production alerts |
error | Error messages only | Production error tracking |
Example:
LOG_LEVEL=debug npm run devThis enables detailed logging for MCP endpoints, rate limiting, and middleware operations.
Social Login (Google/GitHub)
The “Continue with Google” and “Continue with GitHub” buttons on the login and
signup pages are not configured out of the box in local dev — each requires
its own OAuth app registration and environment variables, and Google also
needs a one-time supabase/config.toml change (which start-local-supabase.sh
can apply automatically).
Full setup steps, required env vars, and the GitHub-vs-Google caveat live in
the parent repo’s docs/social-login-local-setup.md.
Local Endpoints
| Service | URL | Notes |
|---|---|---|
| Next.js App | http://localhost:3000 | Your application |
| Supabase Studio | http://127.0.0.1:54323 | Database UI, table editor, SQL editor |
| Supabase API | http://127.0.0.1:54321 | PostgREST API endpoint |
| Database | postgresql://postgres:postgres@127.0.0.1:54322/postgres | Direct psql access |
| Inbucket | http://127.0.0.1:54324 | Catches auth emails (confirmations, resets) |
Common Workflows
Switching multi-org User’s Organization
In psql or Supabase Studio SQL editor:
UPDATE users
SET current_organization_id = '22222222-2222-2222-2222-222222222222'
WHERE email = 'multi-org@test.local';Refresh the browser to see the new org’s data. Switch back with:
UPDATE users
SET current_organization_id = '11111111-1111-1111-1111-111111111111'
WHERE email = 'multi-org@test.local';Creating a New Test User
INSERT INTO auth.users (id, instance_id, email, encrypted_password,
email_confirmed_at, aud, role, raw_user_meta_data, created_at, updated_at)
VALUES (
gen_random_uuid(), '00000000-0000-0000-0000-000000000000',
'newuser@test.local', crypt('password123', gen_salt('bf')),
now(), 'authenticated', 'authenticated',
'{"full_name": "New User"}', now(), now()
);Then get the UUID and create the public user and org membership:
SELECT id FROM auth.users WHERE email = 'newuser@test.local';
INSERT INTO users (id, email, full_name, organization_id, current_organization_id)
VALUES ('<uuid>', 'newuser@test.local', 'New User',
'11111111-1111-1111-1111-111111111111',
'11111111-1111-1111-1111-111111111111');
INSERT INTO organization_members (user_id, organization_id, role)
VALUES ('<uuid>', '11111111-1111-1111-1111-111111111111', 'member');Inspecting RLS as a Specific User
SET LOCAL ROLE authenticated;
SELECT set_config('request.jwt.claims',
'{"sub":"aaaa0001-0001-0001-0001-000000000001"}', true);
SELECT name FROM mcp_servers;
RESET ROLE;Resetting the Database
supabase db reset
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -f scripts/seed-local.sqlRunning Tests
supabase test db
npm test
npm run test:api
npm run test:actionsScript Reference
start-local-supabase.sh
| Flag | Description |
|---|---|
--quick | Skip all prompts (no data pull, sample seed, no dev server) |
--env ENV | Auto-select environment to pull data from (production, staging, dev) |
--no-seed | Skip database seeding entirely |
--no-dev-server | Skip the Next.js dev server prompt |
--help | Show help |
./scripts/start-local-supabase.sh --quick
./scripts/start-local-supabase.sh --env staging
./scripts/start-local-supabase.sh --no-seedstop-local-supabase.sh
| Flag | Description |
|---|---|
--clean | Remove all data dumps and env files without prompting |
--help | Show help |
./scripts/stop-local-supabase.sh
./scripts/stop-local-supabase.sh --cleanPulling Remote Data
You can optionally pull data from production, staging, or dev environments.
One-Time Setup
supabase login
cp .supabase-projects.json.example .supabase-projects.jsonEdit .supabase-projects.json with your project refs from the Supabase Dashboard:
{
"production": "your-production-ref",
"staging": "your-staging-ref",
"dev": "your-dev-ref"
}Then start with a specific environment:
./scripts/start-local-supabase.sh --env stagingRemote data dumps may contain real user data. They are gitignored and must NEVER be committed. The .supabase-projects.json file is also gitignored.
Rate Limiting in Development
Rate limiting is automatically disabled in development mode to make local testing easier.
How It Works
Rate limiting is only enforced when NODE_ENV=production:
if (process.env.NODE_ENV !== 'production') {
// Skip rate limiting
return { allowed: true }
}In Development:
- Unlimited requests to all MCP endpoints
- No rate limit headers in responses
- No 429 errors
- Logging shows “Rate limiting disabled (not production)”
In Production:
- Public endpoints: 100 requests/minute per IP
- Platform endpoints: 500 requests/minute per organization
- Rate limit headers included in all responses
- 429 errors when limits exceeded
Testing Rate Limits Locally
To test rate limiting in development, temporarily set NODE_ENV:
NODE_ENV=production npm run devRemember to unset NODE_ENV after testing, or rate limiting will continue to apply during development.
Troubleshooting
Docker is not running
Start Docker Desktop and wait for it to fully initialize before retrying.
supabase start fails or hangs
supabase stop
docker system prune -f
supabase startPort already in use
lsof -i :54321
supabase stopLogin fails with “Invalid login credentials”
Verify the seed ran successfully:
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres \
-c "SELECT email FROM auth.users;"If empty, re-run the seed:
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres \
-f scripts/seed-local.sqlAdmin UI not accessible for admin@test.local
Verify the env var is set:
grep A2D_PLATFORM_ADMIN_EMAILS .env.localIf missing, add it and restart the dev server:
echo "A2D_PLATFORM_ADMIN_EMAILS=admin@test.local" >> .env.localComplete fresh start
./scripts/stop-local-supabase.sh --clean
docker system prune -af --volumes
./scripts/start-local-supabase.sh --quick
cp .env.supabase .env.local
npm run devSafety Rules
Never commit secrets or production data. The following files are gitignored for your protection.
| File | Gitignored | Safe to Commit |
|---|---|---|
.supabase-projects.json | Yes | No — contains project refs |
.env.supabase / .env.local | Yes | No — contains API keys |
.local/*.sql | Yes | No — may contain production data |
scripts/seed-local.sql | No | Yes — contains only fabricated test data |
Deployed environments apply migrations automatically on deploy — never seed a deployed environment. See Migrations run automatically on deploy below.
Deployed schema is CLI-managed
The Supabase CLI is the source of truth for the deployed database schema. Schema changes
go through supabase migration new + supabase db push only — never by applying SQL
by hand in the Supabase Studio editor. Direct-in-editor changes create drift: the remote
migration ledger (supabase_migrations.schema_migrations) stops matching the files in
supabase/migrations/, and the next supabase db push either fails or silently skips a
change.
If a deployed environment was built via the SQL editor (its migration ledger does not
record the migration files), it must be recovered to a clean CLI baseline before automated
supabase db push on deploy can be enabled.
The one-time recovery is codified in a2d-portal/scripts/recover-migration-state.sh
(dry-run by default; pass --apply to execute):
# Preview what recovery would do (no changes):
./scripts/recover-migration-state.sh --env dev
./scripts/recover-migration-state.sh --env production
# Execute recovery (dev first, then production):
./scripts/recover-migration-state.sh --env dev --apply
./scripts/recover-migration-state.sh --env production --applyThe script picks its strategy per migration from the live database state rather than the
environment name. It probes each migration’s marker object (its first CREATE TABLE or
ADD COLUMN) against the live database and resolves to one of:
- reconcile — every migration’s object already exists (editor-built): mark each applied
with
supabase migration repair— no SQL is re-run. - push — no application schema exists at all (a genuinely fresh target): execute the migrations for the first time.
- mixed — some migrations are applied (objects present) and some were never run (objects
absent): reconcile the present ones,
repair --status revertedany absent-but-recorded ones, thendb pushonly the absent ones.
Do not classify a deployed database from a single “does one core table exist?” check. A2D’s
dev environment was mixed — most migrations had been applied via the editor, but the
newest two had never run. Marking those two “applied” without executing their SQL strands
them permanently (db push then reports “up to date” and never creates their objects). The
recovery script’s per-migration probe handles this; a blanket reconcile does not.
Never run a whole-schema supabase db push onto a database whose schema already exists —
the migrations use plain CREATE TABLE, so the push fails on the existing objects. Reconcile
the ledger instead (the recovery script does this automatically; mixed only pushes the
subset of migrations whose objects are genuinely absent).
Recovery is complete when supabase db diff --linked is empty and
supabase db push --linked --dry-run reports zero pending migrations on both environments.
Migrations run automatically on deploy
Once an environment is on a clean CLI baseline (see above), pending migrations are applied
as part of every deploy — no manual supabase db push step. A single script,
a2d-portal/scripts/deploy-migrate.sh, runs in both deploy contexts, distinguished only by
environment-scoped secrets:
- Vercel (prod): the
buildCommandinvercel.jsonruns the script beforenext build(bash scripts/deploy-migrate.sh && npm run build), applying pending migrations to the production Supabase project. - Heroku (dev): the
release:phase inProcfileruns the same script, applying pending migrations to the dev project.
The script runs supabase db push --db-url "$SUPABASE_DB_URL" — no link, no access token,
no seeding — using the Supabase CLI pinned as a devDependency (invoked via npx, so
Vercel, Heroku, and local all match). A migration failure returns a non-zero exit that
fails the deploy, so code never ships against an un-migrated database.
On Vercel the script runs only on production deploys. It exits early when VERCEL_ENV
is set to anything other than production (preview and branch builds), so preview deploys
never migrate the production database.
Required secret (environment-scoped): set SUPABASE_DB_URL to the project’s direct
connection string — the prod value in Vercel (Production scope), the dev value in
Heroku config vars. Use the direct connection (db.<ref>.supabase.co:5432) or a session-mode
pooler — not the :6543 transaction pooler, which cannot run migration DDL — and
percent-encode the password. The NEXT_PUBLIC_SUPABASE_* keys and SUPABASE_SERVICE_ROLE_KEY
cannot run migrations; they are PostgREST/Auth keys, not a Postgres connection.
Because the new schema is applied during the deploy window, migrations should be backward-compatible with the currently-running code (use an expand/contract sequence for breaking changes). Preview what a deploy would apply with:
npx supabase db push --db-url "$SUPABASE_DB_URL" --dry-runNext Steps
- Database Setup — production Supabase setup
- MCP Servers — learn about creating MCP servers
- Agent Cards — learn about A2A agent cards
- Architecture: Multi-Tenancy — understand the RLS model
Ready to start? Run ./scripts/start-local-supabase.sh --quick and you are up in under 5 minutes.