Supabase Integration¶
NON-NORMATIVE. Setup and usage guide for Supabase in the morphism app.
Overview¶
Morphism uses two Supabase projects under the morphism-systems org:
| Project | Region | Purpose |
|---|---|---|
morphism-production |
AWS us-east-2 | Production traffic |
morphism-staging |
AWS us-east-1 | Dev and preview deployments |
Both projects are on the Free plan (Nano compute) with daily backups (7-day retention).
Provisioned tables (production): access_grants, agents, api_keys, assessments, audit_log, credential_versions, credentials, governance_policies, organizations, verification_scans.
Project refs and org IDs are stored in docs/integrations/platform-services.md.
Environment Variables¶
| Variable | Scope | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Client + Server | Project URL (https://<ref>.supabase.co) |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Client + Server | Anon key — enforces RLS |
SUPABASE_SERVICE_ROLE_KEY |
Server only | Service role key — bypasses RLS |
DATABASE_URL |
Server only | Direct Postgres connection string |
SUPABASE_STORAGE_BUCKET_PUBLIC |
Server only | Public storage bucket name |
SUPABASE_STORAGE_BUCKET_PRIVATE |
Server only | Private storage bucket name |
For local development, pull variables from the Vercel project (staging values):
vercel env pull .env.local
Never hardcode keys. See .env.local.example for the full variable list.
SSR Client Setup¶
The @supabase/ssr package is used throughout. Two client factories are exported from packages/shared/src/supabase/server.ts and re-exported via apps/morphism/src/lib/supabase/server.ts.
createSupabaseClient() — Server Components and Route Handlers. Uses createServerClient with cookie forwarding and sets RLS context from the Clerk session:
import { createSupabaseClient } from '@/lib/supabase/server'
const supabase = await createSupabaseClient()
const { data } = await supabase.from('organizations').select('*')
This function automatically calls set_clerk_org_id RPC to scope queries to the current org, and set_github_username RPC for credential access control.
createAdminClient() — Server-only operations that need to bypass RLS (webhooks, background jobs). Uses the service role key and does not persist sessions:
import { createAdminClient } from '@/lib/supabase/server'
const admin = createAdminClient()
const { data } = await admin.from('audit_log').insert(entry)
There is no browser client factory — all client-side data fetching goes through Next.js Route Handlers that use the server client.
RLS Configuration¶
All tables have Row Level Security enabled. Policies use two RLS context functions set by createSupabaseClient():
set_clerk_org_id— org-scoped access viaorg_idcolumnset_github_username— credential-level access viagithub_usernamecolumn
Example policy pattern:
CREATE POLICY "Users see own org data"
ON organizations
FOR SELECT
USING (id = current_setting('app.clerk_org_id', true)::uuid);
Always use createSupabaseClient() (not createAdminClient()) for user-facing queries so RLS is enforced.
Migration Pattern¶
Migrations are applied with the Supabase CLI:
# Apply pending migrations to staging
supabase db push --project-ref <staging-ref>
# Apply to production
supabase db push --project-ref <production-ref>
Project refs are in docs/integrations/platform-services.md. Keep migration files in supabase/migrations/. Always apply to staging first and verify before promoting to production.
Staging vs Production¶
Switch between projects by setting different env vars. The .env.local.example file defaults to the staging project. Vercel environments are configured separately:
| Vercel Env | Supabase Project |
|---|---|
| Development | Staging |
| Preview | Staging |
| Production | Production (morphism-production, us-east-2) |
To verify which project is active locally:
echo $NEXT_PUBLIC_SUPABASE_URL
# Should contain the staging project ref for local development
Health Check¶
The /api/health endpoint does not query Supabase directly. To verify the database connection manually:
const supabase = await createSupabaseClient()
const { error } = await supabase.from('organizations').select('id').limit(1)
if (error) console.error('Supabase unreachable:', error.message)
Or using the Supabase CLI:
supabase db ping --project-ref <staging-ref>
File Storage¶
Default storage provider is Supabase Storage (configured via STORAGE_PROVIDER=supabase-storage). Two buckets:
morphism-public— public assets (no auth required to read)morphism-private— user uploads (requires signed URLs)
Signed URLs expire after UPLOAD_SIGNED_URL_TTL_SECONDS (default 600s). Max upload size is UPLOAD_MAX_BYTES (default 25 MB).