Decision 0016
A server-only backend kit, starting with guest accounts
accepted · 2026-11-08
Context
Every app needs the same backend pieces, starting with knowing who a visitor is, and hosted auth services charge for that past their free tiers. The first need is guest accounts: a visitor uses an app without signing up, and their data is still there when they come back. Sign-up comes later, and guests must be able to keep their data when it does.
Decision
A library, not a service.
packages/kit, published to npm with the project's version. Apps import it; Spotcraft runs no auth service.Server-only, for SSR apps. The browser never talks to auth or the database. It only holds an
httpOnlysession cookie that the app's server sets in its normal responses.- No auth HTTP routes are mounted and there is no client SDK. The kit uses Better Auth only from server code.
- Cookies are
httpOnly,Secure(except onlocalhost),SameSite=Lax, path/. - The package's
browserexport throws, so a client bundle that imports the kit fails to build.
Guests on Better Auth's anonymous plugin.
createBackend({ databaseUrl, baseURL, secret })returns:db, Drizzle over Postgres, where the app adds its own tables;guest(request, { clientAddress }), which returns the current guest or creates one with a session that lasts about a year and renews on use.
The kit rate-limits guest creation per client address itself, because it mounts none of Better Auth's routes, where Better Auth's own limiter applies.
Spotcraft owns the
authschema, as Supabase does:- The kit holds the migrations (
/schema) and the provisioning code (/provision). - The agent applies the migrations on the server and the CLI applies them locally (ADR 0017).
migrate()applies them to any other Postgres. - The provisioning role owns
auth. The app's role can read and write its rows and referenceauth.users, but can't change its structure. The app ownspublicand its own migrations. - Migrations are tracked in
auth.kit_migrationsand only ever add. Kit X.Y needs Spotcraft X.Y or later, and the kit refuses to start against an older schema.
- The kit holds the migrations (
Tables:
auth.users,auth.sessions,auth.accountsandauth.verifications, with plural snake_case names,uuidids andtimestamptztimes. Guests and registered users shareauth.users(is_anonymous), so sign-up later needs no table change.App data lives in the app's tables, keyed by
user_id uuid references auth.users(id) on delete cascade, never as extra columns onauth.users. Deleting a user removes their data.
Consequences
- A guest who clears cookies or switches device loses access to their data until sign-up exists.
- Client-side-only apps can't use the kit.
- The kit's tables follow Better Auth's model; Better Auth upgrades that change it need an added migration.
- To confirm while building, and amend here if wrong:
- guests get a placeholder email (
emailis required); uuidids work together with the renamed tables;- a guest can be created server-side with the cookie returned.
- guests get a placeholder email (