ADR-002: Better Auth with Google-Only MVP
Decision record for choosing Better Auth over Clerk, Auth0, Firebase, Auth.js, and Supabase Auth.
Status: Accepted Date: 2026-03-21 (Week 0 Decision Sprint), modified 2026-03-26 (TRO-9 implementation) Deciders: Kyle Olson (Solo Founder)
Decision
- Auth provider: Better Auth (self-hosted TypeScript library, MIT licensed)
- Social login: Google-only for MVP; additional providers added close to launch
- Organization model: Better Auth's organization plugin with three tiers (personal/family/company)
- Session storage: Database-backed sessions in Cloud SQL via Drizzle adapter
- MCP auth: Personal Access Tokens (PATs) for Claude Code integration; OAuth 2.1 deferred to post-MVP
- Deferred: Email+password, 2FA/MFA, bot protection (Turnstile)
Context
Trovella needed authentication for a multi-tenant SaaS with three account tiers (personal, family, company). The auth provider had to:
- Store all auth data in the existing Cloud SQL PostgreSQL instance -- no separate auth database
- Provide a built-in organization/multi-tenancy model mapping to the three-tier structure
- Run inside the Next.js app with no separate auth service to operate
- Work with Server Components and the App Router without SSR friction
- Support React Native/Expo for future mobile app (Phase 2)
- Cost $0 at any user scale
Social-only MVP was chosen to reduce auth implementation from 5-8 days to approximately 1-2 hours by eliminating email verification, password reset flows, and 2FA plugin setup. MFA is handled entirely by Google while social-only.
Alternatives Considered
Clerk
Gold-standard DX with pre-built UI components and 15-30 minute setup. Excellent React Native support.
Rejected: $825/month at 50K MAU, $9,825/month at 500K MAU ($118K/year). The development time advantage breaks even within 5-8 months of operating above 50K MAU. High vendor lock-in -- password hashes not exportable, all UI components must be rebuilt on migration.
Firebase Identity Platform
Zero cost for email/social login at any scale. Excellent local emulator and native GCP integration.
Rejected: Significant SSR friction -- no auth() for Server Components, ID tokens expire hourly requiring refresh logic, tenantId not persisted across reloads. The daily SSR friction compounds across hundreds of auth-dependent code paths.
Auth.js v5 (formerly NextAuth)
Zero cost, excellent SSR support with auth() in Server Components, Edge Runtime compatible.
Rejected: No React Native SDK at all (Phase 2 blocker). No built-in organizations -- multi-tenancy requires 1-2 weeks of DIY development. The March 2025 Next.js middleware security vulnerability demonstrated that DIY auth middleware is risky.
Auth0
Most established enterprise platform with comprehensive identity management, SAML SSO, and SCIM.
Rejected: $35,000/month at 500K MAU ($420K/year). Free tier only covers 25K MAU.
Supabase Auth (GoTrue)
Free auth at scale with an RLS pattern identical to Trovella's (SET LOCAL + current_setting()).
Rejected: Requires $25/month Supabase Pro plan including an unused managed PostgreSQL. Auth data in a separate database from application data means two backup strategies and no cross-table joins.
Implementation Decisions
Organization plugin maps to three-tier tenancy
Better Auth's organization plugin provides organization, member, and invitation tables with role-based membership (owner/admin/member). Each Trovella tier is a Better Auth organization with a type metadata field. This eliminated 1-2 weeks of custom multi-tenancy development.
Personal org auto-created via ensurePersonalOrganization()
Initially implemented in Better Auth's databaseHooks.user.create.after, but that fires before the session is fully established. Moved to an idempotent helper called from DashboardPage on first page load. See Sign-In Flow for the full sequence.
nextCookies() plugin lives in the route handler
Better Auth's nextCookies() plugin imports next/headers which only works inside a Next.js app context. Placed in the route handler at apps/web/src/app/api/auth/[...all]/route.ts, not in the shared @repo/auth package.
TEXT IDs throughout
Better Auth generates TEXT-format IDs by default. All application tables use TEXT primary keys to avoid ::uuid casting at join boundaries. See Data & Storage -- Table Conventions.
PATs for MCP auth
trov_ prefix for identification, SHA-256 hashed in storage, shown once at creation. User-scoped (not tenant-scoped). See Personal Access Tokens for the full design.
Rate limiting and CSRF via Better Auth built-ins
30 requests per 60-second window (in-memory storage). CSRF origin validation is built in. Bot protection (Cloudflare Turnstile) is deferred.
Google OAuth test mode
Google OAuth starts in test mode (100-user cap, 7-day token expiry). Basic scopes (openid, email, profile) require no video demo for production approval. Production publish planned for Week 5 of Phase 1.
Consequences
Positive
- $0 auth cost at any scale (MIT-licensed, self-hosted)
- Single database -- auth tables alongside application tables with unified backups
- Built-in multi-tenancy via organization plugin
- Zero vendor lock-in -- all data in own database, standard password hashes when email+password is added
- Social-only MVP eliminated email verification, password reset, and 2FA from initial scope
Negative
- Better Auth is a younger library (~2 years) with a smaller community and documentation gaps
- Schema generation quirks (missing
.defaultNow()onorganization.created_at, import ordering) ensurePersonalOrganizationworkaround needed because database hooks fire before session- Security is the developer's responsibility (no dedicated security team like Clerk/Auth0)
Risks
- Library health -- if Better Auth is abandoned, migration requires rebuilding auth UI and session management (mitigated: all data in own database, only code changes needed)
- Google-only lock-in -- sole login method depends on Google's OAuth terms (mitigated: additional providers planned before public launch)
- PAT security -- long-lived bearer tokens; a leaked PAT grants full API access until revoked (mitigated: SHA-256 hashing, show-once, optional expiration,
lastUsedAtmonitoring)
Validation
| Rule | Enforcement |
|---|---|
| All auth goes through Better Auth | ESLint no-restricted-imports -- only @repo/auth imports better-auth |
Feature endpoints use authorizedProcedure | Architecture test verifies tRPC routers (allowlist for PAT router) |
/api/mcp handles its own auth (PAT) | MCP route bypassed in proxy; PAT validated independently |
| Session cookie not leaked to client bundles | Separate entry points: @repo/auth/server and @repo/auth/client |
References
- Linear: TRO-9 (Authentication), TRO-18 (MCP Server + PAT auth), TRO-48 (OAuth 2.1, deferred)
- Architecture: Auth Flow
- Architecture: Tenant Isolation