Guide
Six suites, 75 tests, zero databases
Most starters test their happy paths and call it done, so the failure that ships is the one nobody drove. Our SvelteKit + Supabase Starter runs 75 tests across six suites without a database or network — services are exercised directly against an in-memory fake Supabase client — and states plainly that the Row Level Security policies are proven separately, against a real instance. This guide shows how the fake works and what each suite pins.
Scope first: the unit suite proves the application business rules. RLS is defense-in-depth and needs supabase start. Both layers being proven is the point; neither substitutes for the other.
The philosophy: test the services, not the stack
Because the services are framework-free, they test clean: an in-memoryfake Supabase client implements the exact fluent query surface the services use — .from().select().eq().is().gt().limit().single().order(),.insert().select().single(), .update()…select().single(), .delete() — stores rows in plain JS maps, and lets tests inspect state and force errors. That buys:
- Speed and isolation. Every suite runs in milliseconds, with no Supabase project needed.
- Proof of the business rules — hierarchy, single-use invites, seat gates — exactly where they live: in the services.
- Honest boundaries. RLS policies are not exercised by the unit suite; they need a real database.
Running
npm install
npm test # vitest run — whole suite
npm run check # svelte-check (TypeScript validation)
npm run build # production buildSuite layout
| File | Coverage |
|---|---|
tests/rbac.test.ts | Matrix pinning, hierarchy helpers, typed errors |
tests/orgs-members.test.ts | Org CRUD, membership management, role hierarchy, removal/leave/transfer |
tests/invites-seats.test.ts | Invite lifecycle, atomic single-use claim, expiry/revoke, seat limits |
tests/billing.test.ts | Mock adapter contract, seat boundaries, env override |
tests/audit.test.ts | Append-only writer, metadata serialization |
tests/smoke.test.ts | Build sanity, constants, adapter instantiation |
tests/helpers/fake-supabase.ts | In-memory Supabase fake + fixture builders (shared, not a test) |
Patterns that carry the suite
Test a service by building a fresh fake per test (beforeEach), seeding users, orgs, and memberships through the fake’s tables, then calling the service and asserting on returned values, thrown codes, and the fake’s state:
const sb = createFakeSupabase();
seedUser(sb, { id: 'u1', email: 'owner@example.com', name: 'Owner' });
const org = await createOrg(sb as any, 'u1', 'Acme Inc.');
// assert org row + membership row + audit entry existRejections are asserted by machine code, never prose:
await expect(setMemberRole(sb as any, { /* … */ })).rejects.toMatchObject({
code: 'hierarchy_violation'
});The fake can also be made to fail on demand — createFakeSupabase({ failNext: /\b(table|action)\b/ }) makes the next call against a matching table throw, so error branches (e.g. org_insert failures bubbling as OrgError) are covered without a mocking library.
RLS-level testing — beyond the unit suite
Application-level rules are fully unit-tested. The RLS policies in supabase/migrations/0001_initial_schema.sql are defense-in-depth and only run against real Postgres:
supabase start # local stack
supabase db reset # apply migrations
supabase test db # SQL-level tests (optional)Verify at minimum: an anon/JWT client cannot read another org’s rows, and that no UPDATE or DELETE policy exists on audit_log. These checks are the difference between “we wrote policies” and “the policies hold.”
Gotchas worth knowing
- The services type their first parameter as
SupabaseClient; tests cast the fake (as any) at the call site — the fake is intentionally untyped at the boundary. audit()logs loudly and does not throw on failure — assert on the fake’s captured log when testing that branch.- Invite tokens are hashed before storage (
sha256); the fake stores the hash, so a test asserting “raw token never stored” greps the fake’sinvitesrows for the raw token and expects no match.
Production checklist
- Exercise the service layer directly with an in-memory fake Supabase client — no database, no network, no SvelteKit runtime.
- Build a fresh fake per test, seed users/orgs/memberships through its tables, then call the service and assert on results, error codes, and state.
- Assert on machine codes, not message strings — code survives copy edits, prose does not.
- Make the fake fail on demand to cover error branches without mocking libraries.
- Split the proof honestly: unit suites prove application rules; a real Supabase instance (supabase start) proves the RLS policies.
- Verify at minimum that an anon/JWT client cannot read another org’s rows, and that no UPDATE or DELETE policy exists on audit_log.
All six lines are implemented in the starter kit. Cross-reference the RBAC guide for how the matrix tests split from the RLS tests, and the versioning guide for how a release is cut once the suite is green.
Related reading
- RBAC & RLS — the two-layer enforcement the tests prove
- Architecture — why services being framework-free makes this possible
- Versioning — cutting a release once the suite is green
- Product page — features, pricing, FAQ
- All documentation — guides, deep dives, and evaluations
Get in touch
Questions about the product, team licenses, or anything else? We'll respond within 48 hours.