Trovella Wiki

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() on organization.created_at, import ordering)
  • ensurePersonalOrganization workaround 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, lastUsedAt monitoring)

Validation

RuleEnforcement
All auth goes through Better AuthESLint no-restricted-imports -- only @repo/auth imports better-auth
Feature endpoints use authorizedProcedureArchitecture 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 bundlesSeparate entry points: @repo/auth/server and @repo/auth/client

References

On this page