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).
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
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 pushreplays every migration onto a fresh project;wrangler deployships 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.varslocally, Cloudflare Secret in prod, never git) keeps the RLS-bypassingservice_rolekey 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
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:
| Component | Purpose | Why this one |
|---|---|---|
| Cloudflare Workers | Backend runtime | Edge-deployed, scales to zero, auto-deploy from GitHub, no server to manage |
| Supabase Postgres | Database + RLS | Managed Postgres with Row Level Security as the isolation primitive |
| Supabase Auth (GoTrue) | Authentication | Anonymous sign-in + Sign in with Apple + real signed JWTs |
| Supabase Storage | File storage | S3-compatible object store with private buckets + signed URLs |
| Supabase CLI | Schema-as-code | db push replays timestamped migrations onto any project |
| wrangler | Deploy + secrets | deploy ships the Worker; secret put sets encrypted runtime vars |
| .dev.vars | Local secrets | Gitignored file feeding c.env during wrangler dev |
| Cloudflare Secret | Prod secrets | Encrypted, injected at runtime; never in wrangler.jsonc or git |
| jose | JWT verification | Verifies 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 importssupabase-swiftand never holds theservice_rolekey. 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/supabase2. 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_SECRETleft empty supabase/config.tomlcommitted with auth toggles
3. Schema
- Migrations authored idempotently in
supabase/migrations/ npm run db:pushapplies cleanly to the linked project- RLS enabled on every user-data table (see infra-supabase-rls)
4. Secrets
.dev.varspopulated locally and confirmed in.gitignore.dev.vars.examplecommitted with placeholders only- All secrets set in Cloudflare as Secret type (
wrangler secret putor dashboard) grep -rn "sb_secret_\|service_role" .returns nothing in tracked files
5. Deploy
npm run typecheckpassesnpm run deploysucceeds;/healthreturns 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 = trueand an anonymous-sign-in rate limit set- Edge Rate Limiting / WAF in front of the Worker
pg_cronjob purging empty, expired anonymous users- Workers observability enabled; a staging project separate from production
More in Backend
- AWS Infrastructure with CDKFreeProduction-ready AWS infrastructure using CDK — App Runner for HTTP services, Aurora Serverless for database, Cognito for auth, Lambda for serverless functions, and S3 for storage
- Supabase Row Level Security (RLS) Data IsolationFreeProduction-ready Row Level Security patterns for a Supabase + Cloudflare Workers stack — the database-layer isolation primitive that makes cross-user data leaks impossible even when a route is buggy or someone talks to PostgREST directly. Covers the owner-isolation table skeleton (1:1 profile + one-to-many children), the five load-bearing RLS rules ((select auth.uid()) wrapping, TO authenticated, forward-set child subqueries, indexing policy columns, trusting only auth.uid()/app_metadata), the service_role (bypass RLS) vs user-JWT (RLS enforced) client split, server-side owner_id injection, public read-only tables whose writes funnel through service_role, and a two-token cross-access isolation test that must return 403/empty.
- Supabase Storage Private Buckets + Signed URLsFreeProduction-ready private file storage for a Supabase + Cloudflare Workers stack — a private bucket created as an idempotent SQL migration, server-side service_role uploads (the client never talks to Storage and never holds the service key), and on-demand signed URLs with short TTLs. IDOR-proof by construction — the object path is derived from the verified JWT ({userId}/{itemId}.jpg, never client-supplied) and ownership is checked through RLS before any URL is signed — with defense-in-depth RLS policies on storage.objects, orphan-object cleanup (Postgres ON DELETE CASCADE never touches Storage), and the pitfalls of public buckets and leaked signed URLs.
- TikTok Ad Attribution with RevenueCat (Dual-Channel)ProFull-depth TikTok attribution for RevenueCat apps — client SDK reports the purchase moment, your backend forwards renewals/refunds from RC webhooks to TikTok Events API. Ships the complete Workers forwarder, device-identifier relay chain (ATT → RC → webhook → Events API), ASN wiring, and a battle-tested verification SOP (iOS + Cloudflare Workers)
- TikTok Ad Attribution & TrackingProTikTok ad attribution module with ATT permission flow, standard/custom event tracking, and debug logging — bridges to TikTok Business SDK via closure injection. Includes complete integration guide with credentials setup, App Store compliance, and campaign launch workflow (iOS only)