Sprint 1 — Auth got the foundation in
Sprint 1 closed today — one week in advance, since I’m posting Monday morning and closed Sprint 0 on Sunday evening — and the main thing I have to say about it is that auth works in production but row-level security, the thing I most wanted to land, doesn’t actually enforce yet. The schema is in. The policy is in. The RLS bit on the right table is set to true. None of it does anything. I’ll explain.
What shipped
- Cloud accounts wired up: Neon, Vercel, Resend. Free tier on all three; Domi is solo-dev-on-the-side scale. Neon’s free tier didn’t expose Montreal (
ca-central-1), so I’m temporarily on Ohio (us-east-2) and made a note to migrate before V1 ships, since Canada-first data residency is locked. - Custom domain
domiapp.ailive, apex canonical, Vercel auto-issuing the SSL cert.wwwredirects to apex via a 307. I bought the domain through Vercel itself, which means its DNS lives in Vercel’s panel — different from this blog atgailleur.com, where DNS is at GoDaddy. Both work; just have to remember which is which when adding TXT records for things like Resend’s domain verification. - Schema for auth and tenancy — six tables (
users,accounts,sessions,verification_tokens,tenants,memberships), one Postgres enum (member_rolewithowner | member | viewer | guestper the Role Permission Matrix), one RLS policy onmemberships. Drizzle-generated migration applied to Neon staging. Verified end-to-end with aselectagainstpg_tablesandpg_policies. PR #10. - Auth.js v5 wired up with the Drizzle adapter binding to that schema. Magic-link via Resend works end-to-end on
https://domiapp.ai. Averification_tokensrow inserts when a magic link is requested; clicking the link creates ausersrow and asessionsrow. I tested it against my real inbox. PR #11. - CI green throughout, with all three Vercel checks (build, preview deploy, preview comments) passing on each PR.
Where the data lives
This is family data eventually, so residency matters. As of end-of-Sprint-1:
- Compute (Vercel functions):
yul1— Montreal. Caught this in a response header (x-vercel-id: yul1::…) and was pleasantly surprised. Vercel routes function execution to the nearest region, and from my POP that’s their Montreal site. The request lifecycle that touches my data runs inside Canadian borders. - Postgres (Neon):
aws-us-east-2— Ohio. The deviation I’m carrying. Free tier didn’t exposeaws-ca-central-1(Montreal); rather than wait, I provisioned in Ohio and made a note. Requirements §17 of the spec commits me to Canadian residency before public launch, so this gets migrated before V1 ships — likely as one of the first paid-tier moves. - Email transit (Resend):
us-east-1— N. Virginia. Resend rides on top of Amazon SES, which is region-bound, and they don’t expose a Canadian region today. Less of an issue than the DB because email content is transit-only (the magic-link body is a token + a URL, not personal data) and the audit trail of who got an email lives in Postgres, not Resend.
Two of three are outside Canadian borders for V1 dogfood. Acceptable while I’m the only user; needs to be closer to right before anyone else’s data lands.
What I cut
- Google OAuth credentials. The provider code path is in
apps/web/src/lib/auth.tsand lights up as soon asGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETexist in the env. Creating the OAuth client in Google Cloud Console wasn’t on Sprint 1’s critical path — magic-link is enough for me to dogfood — so I deferred the fifteen minutes of console-clicking to whenever I want it. - Anthropic API key, Sentry project, Cloudflare account. None of these block auth; they’re for Sprint 2 (LLM abstraction needs Anthropic), Sprint 3 (R2 needs Cloudflare), and “whenever I have errors” (Sentry). Pulling them into Sprint 1 would have eaten provisioning time without enabling anything.
- Custom signin page. Auth.js v5’s default page looks fine. A themed signin page is brand work, not foundation work, and brand work is locked behind the public-name decision at Sprint 24.
What surprised me
Postgres RLS doesn’t enforce when you connect as a superuser. I knew this in the abstract; I forgot it in practice. Neon hands you a neondb_owner connection string by default, and neondb_owner is a superuser, and superusers bypass row-level security regardless of how many policies you’ve defined. The schema PR landed with ALTER TABLE memberships ENABLE ROW LEVEL SECURITY and a CREATE POLICY that says tenant_id = current_setting('app.tenant_id', true)::uuid. From pg_policies it looks like everything’s there. From an actual query, the policy does nothing. Sprint 2 fixes this with a non-superuser app_role plus per-request set_config('app.tenant_id', …) plumbing in the route handlers. The foundation is in; the foundation just isn’t bearing weight yet.
Auth.js v5’s drizzle adapter looks up OAuth fields by JS property name, not column name. The official Auth.js schema example uses snake_case JS properties (refresh_token, access_token, expires_at, etc.) for the OAuth-spec-named fields on accounts. I had used camelCase out of habit (refreshToken, accessToken). Mid-wiring, the adapter started silently dropping fields. Renamed the JS properties; the column names in the DB stayed the same; drizzle-kit generate confirmed it as a no-op migration. Worth knowing if you ever wire Auth.js to a custom Drizzle schema: match the property names the adapter expects, not what the column looks like.
Resend’s email setup is the cleanest I’ve ever done. Add the domain in their dashboard, paste four DNS records (SPF, DKIM, DMARC, and a return-path) at your registrar, click “verify”. That’s it. Twenty minutes start-to-finish, and that includes DNS propagation time on a Sunday afternoon. The v=spf1 include:amazonses.com ~all SPF record, the DKIM TXT, the _dmarc policy, and the return-path are all generated for you with sane defaults — you do not need to hand-craft a DMARC policy or know what a return-path is to get a verified, deliverability-clean sending domain. If you’ve ever stood up email-sending infrastructure directly on Amazon SES (or, god help you, on a bare SMTP server) the difference is dramatic. Solo founders, this is the right tier of abstraction. Genuine credit to the Resend team — they built the email-deliverability onboarding I wish every category had.
Where Sprint 2 picks up
Two pieces:
- Finish M1. Create a non-superuser
app_roleon Neon, switch the runtime connection to it, plumbset_config('app.tenant_id', …)into the request lifecycle so the RLS policy actually enforces. Optional bonus: provision Google OAuth credentials. - Start M2. First scaffold of the LLM abstraction layer in
packages/shared/src/llm/, the role-based router (chat / classify / extract_document / generate_plan / predict_task / sensitive_context / background_automationper the spec), an Anthropic adapter, and the first real eval against a fixture ineval-set/classify/.
If both ship in ten hours next week, I’m on schedule.