Supabase + Cloudflare Workers Infrastructure

Production-ready serverless infrastructure using Supabase (managed Postgres + Auth + Storage) and Cloudflare Workers (Hono backend) — the zero-VPC, zero-server counterpart to the AWS CDK stack. Covers Supabase CLI migrations, the immutable-region rule, secret management (.dev.vars / Cloudflare Secret, never git), Workers deployment via wrangler, and dual-mode JWT verification (JWKS ES256 preferred, HS256 fallback).

Free
Free with a ShipSwift account.

Copies a short instruction with your private download link. Paste it into your agent: it downloads the whole skill (the source code, the setup steps and the mistakes we already made for you) and builds the screen.

Works with

+70

What it does

Manually clicking through the Supabase dashboard and pasting SQL into the SQL Editor is error-prone, unrepeatable, and impossible to version control. As your app grows you will need to reproduce your schema for a staging project, a second app, or a fresh region — and "remember which buttons I clicked" does not scale.

This skill defines the entire Supabase + Cloudflare Workers stack as code:

  • Reproducibility — supabase db push replays every migration onto a fresh project; wrangler deploy ships the backend from source
  • Version control — schema changes are timestamped SQL files in git, reviewed in the same PR as application code
  • No servers, no VPC — Supabase is managed Postgres/Auth/Storage over HTTPS; Workers run at the edge. There is nothing to patch, no NAT Gateway, no connection pool to size
  • Secret hygiene by construction — a single rule (.dev.vars locally, Cloudflare Secret in prod, never git) keeps the RLS-bypassing service_role key out of every tracked file
  • Cost floor near zero — both platforms have generous free tiers; you pay per request, not per idle hour

This is the serverless counterpart to infra-cdk. Read them side by side: where CDK provisions a VPC + Aurora + App Runner + Cognito + API Gateway (rich control, ~$80/mo floor, minutes to deploy), this stack trades that control for a managed BaaS that has no idle cost and deploys in seconds. Choose CDK when you need VPC-private resources, fine-grained IAM, or AWS-native services; choose Supabase + Workers for indie iOS apps that want the shortest path from git push to production.

What you get

  • ✓246 lines of source code, ready to paste
  • ✓Architecture
  • ✓Dependencies
  • ✓Implementation
  • ✓Integration Checklist
  • ✓Common Customizations
  • ✓Known Pitfalls
SupabaseCloudflare WorkerswranglerHonoinfrastructuremigrationsSupabase CLIsecretsJWKSHS256deploymentserverlessPostgresRLS

Architecture

┌──────────────────────────────────────────────────────────────────┐
│                          iOS Client                                │
│              (URLSession only — never talks to Supabase)           │
└──────────────────────────────┬─────────────────────────────────────┘
                               │ HTTPS (REST + Bearer JWT)
                               ▼
┌──────────────────────────────────────────────────────────────────┐
│                 Cloudflare Workers  (Hono backend)                 │
│   src/index.ts → routes → middleware (verify JWT) → supabase-js    │
│   compatibility_flags: ["nodejs_compat"]                           │
└───────────────┬───────────────────────────────┬────────────────────┘
                │ service_role (bypass RLS)      │ user JWT (RLS on)
                ▼                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                        Supabase project                            │
│   Auth (GoTrue)   ·   Postgres + RLS   ·   Storage (private)       │
│   region: chosen at creation — IMMUTABLE                           │
└──────────────────────────────────────────────────────────────────┘

        Deploy / provision path (two independent pipelines):
        ┌─────────────────────┐        ┌──────────────────────────┐
        │  supabase CLI       │        │  wrangler / GitHub CI     │
        │  db push → Postgres │        │  deploy → Workers runtime │
        └─────────────────────┘        └──────────────────────────┘

Component roles:

ComponentPurposeWhy this one
Cloudflare WorkersBackend runtimeEdge-deployed, scales to zero, auto-deploy from GitHub, no server to manage
Supabase PostgresDatabase + RLSManaged Postgres with Row Level Security as the isolation primitive
Supabase Auth (GoTrue)AuthenticationAnonymous sign-in + Sign in with Apple + real signed JWTs
Supabase StorageFile storageS3-compatible object store with private buckets + signed URLs
Supabase CLISchema-as-codedb push replays timestamped migrations onto any project
wranglerDeploy + secretsdeploy ships the Worker; secret put sets encrypted runtime vars
.dev.varsLocal secretsGitignored file feeding c.env during wrangler dev
Cloudflare SecretProd secretsEncrypted, injected at runtime; never in wrangler.jsonc or git
joseJWT verificationVerifies Supabase JWTs against JWKS (ES256) or the legacy secret (HS256)
iOS stays dependency-free: the client only ever talks to the Worker over REST. It never imports supabase-swift and never holds the service_role key. The Worker is the only thing that touches Supabase. See auth-supabase-anonymous.

Integration Checklist

1. Tooling

npm install hono @supabase/supabase-js jose zod
npm install --save-dev wrangler @cloudflare/workers-types typescript
brew install supabase/tap/supabase

2. Supabase project (one-time)

supabase login
# Create the project in the correct region (IMMUTABLE) — dashboard or CLI.
supabase link --project-ref <project-ref>
  • Project created in the correct, immutable region
  • (Recommended) migrated to ES256 signing keys; SUPABASE_JWT_SECRET left empty
  • supabase/config.toml committed with auth toggles

3. Schema

  • Migrations authored idempotently in supabase/migrations/
  • npm run db:push applies cleanly to the linked project
  • RLS enabled on every user-data table (see infra-supabase-rls)

4. Secrets

  • .dev.vars populated locally and confirmed in .gitignore
  • .dev.vars.example committed with placeholders only
  • All secrets set in Cloudflare as Secret type (wrangler secret put or dashboard)
  • grep -rn "sb_secret_\|service_role" . returns nothing in tracked files

5. Deploy

  • npm run typecheck passes
  • npm run deploy succeeds; /health returns 200
  • (Optional) GitHub connected for auto-deploy on push

Known Pitfalls

1. Supabase project region is immutable

Symptom: latency is poor for your users, or a compliance requirement is unmet, and you cannot change it.

Fix: the region is chosen at project creation and cannot be changed afterwards. Pick it correctly on step one. Moving regions later means creating a new project and migrating data.

2. Missing nodejs_compat → supabase-js crashes on Workers

Symptom: runtime error about a missing Node built-in (e.g. stream, crypto) as soon as a route calls supabase-js.

Fix: set "compatibility_flags": ["nodejs_compat"] in wrangler.jsonc. supabase-js depends on Node polyfills that Workers only exposes behind this flag.

3. Using process.env instead of c.env

Symptom: undefined config at the edge; secrets never load.

Fix: Workers inject bindings into the Hono context. Read c.env.SUPABASE_URL, never process.env. Type them in Env (src/types.ts).

4. service_role key committed to git or shipped to the client

Symptom: full-database compromise — the key bypasses every RLS policy.

Fix: the key lives only in .dev.vars (gitignored) and Cloudflare Secret. It is never in wrangler.jsonc, never in a tracked file, never in the iOS app. .dev.vars.example carries placeholders only.

5. Secret set as a plaintext "build" var instead of a runtime Secret

Symptom: the secret is visible in the dashboard and/or logs; it does not reach c.env at runtime as expected.

Fix: use wrangler secret put or the dashboard's Variables and Secrets → Secret (encrypted, runtime) section. Do not paste secrets into vars in wrangler.jsonc (those are plaintext, build-time).

6. service_role used to reliably bypass RLS but requests still hit RLS

Symptom: a system operation (e.g. initializing a profile row) is unexpectedly blocked by RLS.

Cause: with the new sb_secret_ key format, passing the key only as the createClient apikey arg does not always set the PostgREST role — requests fall back to an RLS-constrained role.

Fix: put the key explicitly in the Authorization: Bearer header (as serviceClient does above). PostgREST decides the role from Authorization, not apikey.

7. Migration applied by hand diverges from the file

Symptom: a fresh project (or teammate) can't reproduce the schema; something works only on your project.

Fix: every schema change is a file in supabase/migrations/, applied with supabase db push. Never make a change only in the dashboard SQL Editor without capturing it as a migration. Write files idempotently so re-apply is a no-op.

8. npm install -g supabase fails or misbehaves

Symptom: the global npm install of the Supabase CLI errors or is flagged unsupported.

Fix: install the CLI via Homebrew (brew install supabase/tap/supabase) or as a project devDependency. The npm global install path is not supported.

Production Readiness Checklist

  • Project in the correct immutable region, migrated to ES256 signing keys
  • RLS enabled and tested on every user-data table (infra-supabase-rls)
  • Private buckets + signed URLs for any user files (infra-supabase-storage)
  • All secrets are Cloudflare Secret type; nothing sensitive in git or wrangler.jsonc
  • enable_refresh_token_rotation = true and an anonymous-sign-in rate limit set
  • Edge Rate Limiting / WAF in front of the Worker
  • pg_cron job purging empty, expired anonymous users
  • Workers observability enabled; a staging project separate from production

More in Backend