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 build

Suite layout

FileCoverage
tests/rbac.test.tsMatrix pinning, hierarchy helpers, typed errors
tests/orgs-members.test.tsOrg CRUD, membership management, role hierarchy, removal/leave/transfer
tests/invites-seats.test.tsInvite lifecycle, atomic single-use claim, expiry/revoke, seat limits
tests/billing.test.tsMock adapter contract, seat boundaries, env override
tests/audit.test.tsAppend-only writer, metadata serialization
tests/smoke.test.tsBuild sanity, constants, adapter instantiation
tests/helpers/fake-supabase.tsIn-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 exist

Rejections 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’s invites rows 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

Get in touch

Questions about the product, team licenses, or anything else? We'll respond within 48 hours.

Max 2000 characters

Stored in our own database — no third party. Deleted on request.