Skip to content

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 via org_id column
  • set_github_username — credential-level access via github_username column

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).