Docs / Database / Seed & RLS
Seed & RLS
Seeding demo data
bun run db:seed (lib/db/seed.ts) creates one demo team and login against a freshly migrated database: demo@keel.dev / password1234. It signs the user up through Better Auth's own auth.api.signUpEmail, not a raw insert, so the password is hashed correctly and the databaseHooks.user.create.after hook fires, the same hook that creates a personal team, owner membership, and a zero-balance credits row for every real sign-up. The script renames and restocks that team rather than inserting a second one.
Refuses to run against production
The seed script throws immediately if NODE_ENV === "production". demo@keel.dev / password1234 is a fixed, publicly-known login, running the seed against a live DATABASE_URL (an easy mistake since it's the same .env.local the app itself reads) would create a real, sign-in-able backdoor account. This only blocks when something has actually set NODE_ENV=production (a deploy step, a hosting platform), which is exactly the case worth catching, it's unset by default in local dev.
Locking down Postgres's REST surface
If you're on Supabase, it auto-exposes every public schema table to its anon/authenticated PostgREST API keys by default. Drizzle never goes through that API (it connects directly as the table owner, which always bypasses Row Level Security), so this only matters for closing off a surface real client-side Supabase queries would otherwise hit, none exist in Keel yet, everything goes through server-side Drizzle calls.
Run bun lib/db/lock-rls.ts once against a fresh database. It enables RLS on every table in public and revokes all grants from anon/authenticated, RLS enabled with no policies defined means deny-all for those two roles. Both operations are idempotent, safe to re-run after adding new tables. If you later add real client-side Supabase queries, add policies for the specific tables/roles that need them, don't skip running this script instead.