Cinderblock
Forkable multi-tenant SaaS starter on Supabase + Next.js whose load-bearing differentiator is a 74-test pgtap Row-Level-Security suite against a hostile fixture (5 workspaces × 8 users × every role × every table). Cross-tenant reads, writes, joins, CTEs, UNIONs, aggregates all return zero rows.

Overview
Most Supabase multi-tenant deliveries leak. The failure mode is subtle. A policy passes when the owner reads their own data. It breaks when there's a join across tables. Or a subquery that filters by user_id. Or a view that aggregates rows from multiple workspaces. The first multi-tenant customer to actually try cross-tenant data access — an audit, a pentest, an accidental QA query — finds the hole. The freelancer is long gone.
Cinderblock is a forkable Supabase + Next.js multi-tenant SaaS starter built around the inverse contract: every claim about tenant isolation in the docs is backed by a test in supabase/tests/, and the test suite runs against a deliberately hostile fixture.
The hostile fixture and the 74-test contract:
5 workspaces × 8 users with a deliberate membership matrix — overlapping memberships across workspaces, every role variant, one outsider with zero memberships. The suite authenticates as a user with no business reading a given row and asserts an empty result. Tests fail loudly if a single row leaks. Categories: cross-tenant reads (12), cross-tenant writes (8), join leakage through CTEs / UNIONs / subqueries / aggregates (6), security-definer hardening with an end-to-end search-path attack (5), audit-log integrity (4), service-role + connection-pool safety (7), concurrency + impersonation visibility (8), billing seat-cap + writability + Stripe idempotency (5), task surface (5), invitation flow (5).
Append-only audit log at the Postgres grant level:
The cb_audit_writer Postgres role has INSERT-only grants on audit_events and zero SELECT/UPDATE/DELETE grants anywhere. The Next.js server opens a separate postgres-js connection authenticated as this role for audit writes. Even a compromised application server cannot rewrite history. A DB CHECK constraint backstops the TypeScript guard against self-impersonation rows regardless of role.
Doubly-logged admin impersonation:
Server-minted HS256 JWT signed with SUPABASE_JWT_SECRET overrides the session via a cb_impersonate cookie. RLS evaluates as the impersonated user; every audit row records both actor_id (the target) and impersonator_id (the admin), pulled directly from JWT claims. 6-digit OTP step-up before mint. Hard 60-minute expiry, no refresh path. The admin's normal session is preserved untouched in the sb-* cookies.
Insert-first Stripe webhook idempotency:
insert ... on conflict do nothing returning event_id — if RETURNING is empty, the function returns 200 before any side effect. Check-then-insert is racy under Stripe's at-least-once delivery and is documented as the explicit anti-pattern.
Live policy viewer at /docs/security/policies:
Reads pg_policies on the deployed database via a security_invoker view. The page proves the deployed policies are exactly what the docs claim — no static snapshot can drift. Pair with /docs/security/test-results which renders the captured 74/74 green pgtap output from CI.
Stack:
- Next.js 16 + React 19 + Tailwind 4 + TypeScript strict with
noUncheckedIndexedAccess - Supabase Cloud (Postgres 17 + Auth + Edge Functions + Realtime +
pg_cron) - Stripe Checkout + Customer Portal in test mode with insert-first webhook idempotency
- EC2 + Apache (vhost with
ServerAliasfor both subdomains) + PM2 + Let's Encrypt - Standalone Next.js output, rsync deploys, prod
X-Forwarded-HostviaProxyAddHeaders
Quality:
74 pgtap tests, 19 Vitest (units + service-role firewall scan), 5 Playwright public-routes smoke. CI runs all three on every push and PR via GitHub Actions. Forkable template — ./scripts/postclone.sh is idempotent and takes a forker from npm install to green pgtap in under 5 minutes warm cache. Exits non-zero on any red test so a forker cannot accidentally ship a misconfigured deployment.
What it proves:
The same person hand-rolled the data model, wrote 22 RLS policies with hardened security_definer helpers, built the 74-test hostile pgtap suite, scaffolded the Next.js app + docs site with a live policy viewer, implemented HS256 impersonation JWT minting with doubly-logged audits, wired Stripe Checkout / Customer Portal / insert-first webhook idempotency, and deployed all of it to EC2 + Apache + PM2 + Let's Encrypt. The case for hiring me to build a Supabase multi-tenant SaaS correctly the first time, or to audit one that already leaked.
Results
74 pgtap policy tests against a 5×8 hostile fixture — cross-tenant reads, writes, joins, CTEs, UNIONs, aggregates all return zero rows
Append-only audit at the Postgres grant level —
cb_audit_writerrole has INSERT-only and no other grantsDoubly-logged impersonation via a 60-min HS256 JWT — every audit row carries both actor_id and impersonator_id, pulled from JWT claims
Insert-first Stripe webhook idempotency —
on conflict do nothing returninggates every side effectLive policy viewer reads pg_policies on the deployed DB so the docs cannot drift from reality
Gallery


