Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.
Date: 2026-05-10
Status: Accepted
Deciders: Daniel Meza
Context and Problem Statement
Urza's Wallet requires user authentication for saved comparisons, rate alerts, and personalized rankings. The authentication layer must:
- Support email/password registration and login.
- Support social login (Google OAuth at minimum).
- Issue sessions that can be validated server-side in Next.js middleware without exposing tokens to the browser.
- Integrate cleanly with a Railway-hosted Next.js application.
- Comply with Ley 8968 (Costa Rica data privacy): user data must be deletable on request.
- Be operational within a short timeline without specialized identity engineering.
The NerdWallet reference architecture recommends a dedicated CIAM (Customer Identity and Access Management) platform (Auth0, Okta, AWS Cognito, or self-hosted Keycloak) and explicitly warns against rolling custom session management. The question is which provider best fits the Urza's Wallet constraints: team size, Railway deployment model, Costa Rica market, and Phase 1 scope.
Decision Drivers
- No self-hosted infra: Railway is the only cloud platform in scope for Phase 1. Running a self-hosted Keycloak instance on Railway adds operational complexity (database, memory footprint, security patching) disproportionate to our user scale.
- Next.js integration quality: The auth provider must have a production-grade Next.js SDK with App Router support, middleware-compatible session validation, and server component session access.
- Costa Rica market compatibility: No US-only restrictions (CCPA-specific features that break in other jurisdictions), Spanish language support in hosted UI.
- Ley 8968 erasure: The provider must support programmatic account deletion via an admin API so the DeletionHandler workflow can cascade correctly.
- Cost at Phase 1 MAU (Monthly Active Users): With expected launch MAU of 500–5,000, the cost must be predictable and low.
- Time to implementation: A small team should be able to complete a working auth flow in under 2 engineering days.
- Social login: Google OAuth is required at launch. Apple Sign-In is a future requirement.
- MFA (Multi-Factor Authentication): Optional at user discretion in Phase 1; required for admin and editor roles in Phase 2.
Considered Options
- Clerk — managed identity platform with Next.js-first SDK, Railway-compatible, no self-hosting.
- Auth0 (by Okta) — mature CIAM platform, broad ecosystem, Next.js SDK available.
- Supabase Auth — open-source auth built into the Supabase platform; can be used standalone.
- AWS Cognito — AWS-managed identity service; integrates well within AWS but is more complex outside it.
- Keycloak (self-hosted on Railway) — open-source OIDC server; full control; significant ops overhead.
- Custom session auth (Lucia, iron-session, jose) — custom-built session management using open-source libraries.
Decision Outcome
Option 1: Clerk is adopted as the authentication provider for Phase 1.
Rationale:
- Next.js App Router integration is Clerk's primary use case. The
@clerk/nextjsSDK is maintained alongside the Next.js App Router release cycle. Middleware session validation, Server Component session access, and Route Handler token validation all work without workarounds. - Railway deployment is straightforward. Clerk is a SaaS platform accessed via API keys. No Railway service or volume is required for Clerk itself — only the
CLERK_SECRET_KEYandCLERK_PUBLISHABLE_KEYenvironment variables need to be set. - Account deletion API: Clerk provides
clerkClient.users.deleteUser(userId)in the Admin SDK, which the DeletionHandler calls as part of the Ley 8968 erasure workflow. - Pricing: Clerk's free tier supports up to 10,000 MAU with full feature access. Paid tier ($25/month) unlocks custom domains and removes Clerk branding from the hosted login page. Both tiers are within Phase 1 budget.
- Hosted login UI with custom domain: Clerk's hosted sign-in page can be mapped to
auth.fincompare.cr, preserving brand trust. Spanish language localization is supported vialocalizationprop. - Social login and MFA: Google OAuth and optional TOTP (Time-based One-Time Password) are built-in and configurable via the Clerk dashboard without code changes.
- Webhook support: Clerk emits
user.created,user.updated,user.deletedwebhooks that the API listens to for profile synchronization (e.g., email updates from Clerk sync touser_profiles.email).
The integration model:
clerkMiddleware()runs on every Next.js request. It validates the Clerk session cookie and attachesauth()to the request context.- Protected Next.js routes use
auth().protect()in the middleware config. Unauthenticated requests are redirected to Clerk's hosted login page. - The Backend API Service validates Clerk tokens by calling
clerkClient.verifyToken()in the Auth Adapter. Theclerk_user_idfrom the validated token is used as the canonical user identifier throughout the system. - The browser never receives a raw JWT. Clerk's SDK manages HttpOnly session cookies.
Positive Consequences
- Speed to implementation: A working email + Google login flow is achievable in under 1 engineering day using Clerk's Next.js quickstart.
- Security: Clerk handles credential hashing, rate-limiting on login attempts, breach detection, and session rotation. These are not reimplemented in the application.
- Ley 8968 compliance: The DeletionHandler workflow can fully revoke a Clerk user account programmatically, satisfying the right of erasure.
- No Railway ops overhead: Clerk is external SaaS. No Railway service, database, or volume required for auth.
- Future MFA: Enabling TOTP for admin/editor roles in Phase 2 requires only a Clerk dashboard configuration change, not code changes.
Negative Consequences / Trade-offs
- Vendor lock-in: If Clerk's pricing becomes unfavorable or the product is discontinued, migration requires re-implementing session management and user re-enrollment. Mitigation: all Clerk-specific code is isolated in the Auth Adapter component; the
clerk_user_idis stored as an opaque string inuser_profiles— replacing it with a different provider's user ID requires a data migration but not an application rewrite. - Hosted login page: Clerk's hosted login page is on a Clerk-controlled domain (or
auth.fincompare.crwith a custom domain). This creates a redirect during login that some users may find disorienting. Mitigation: use Clerk's embedded<SignIn>component for an in-app login experience if the redirect UX proves problematic. - External dependency for session validation: Every authenticated request makes a call to validate the Clerk session. Clerk's SDK caches validation locally, so actual network calls to Clerk are infrequent, but Clerk's availability is a dependency for all authenticated flows. Mitigation: Clerk maintains 99.99% SLA (Service Level Agreement) uptime; anonymous/public routes are unaffected by Clerk outages.
- Data residency: Clerk stores user identity data on US-based infrastructure. This is acceptable under Ley 8968 if a Data Processing Agreement (DPA) is executed with Clerk before go-live. Clerk provides a standard DPA.
Pros and Cons of the Options
| Option | Next.js App Router | Railway Fit | Ley 8968 DPA | Ops Overhead | MAU Cost (5k) |
|---|---|---|---|---|---|
| Clerk (chosen) | Excellent (primary use case) | SaaS (no Railway service) | Available | None | Free (< 10k MAU) |
| Auth0 | Good | SaaS (no Railway service) | Available | None | ~$23/month (Essentials) |
| Supabase Auth | Good | SaaS or self-hosted | Available | Low (if SaaS) | Free (< 50k MAU) |
| AWS Cognito | Adequate | Requires AWS | Available | Low-medium | ~$0 (< 50k MAU free tier) |
| Keycloak (self-hosted) | Requires manual OIDC integration | Moderate Railway overhead | Full control | High | Infrastructure cost only |
| Custom session auth | Manual implementation | Zero | Full control | High (security risk) | Zero |
Auth0 was the closest alternative. Its Next.js SDK is mature and it offers a generous free tier. The deciding factors against Auth0 were: Clerk's deeper App Router integration (Auth0's Next.js SDK uses an older pattern that requires more boilerplate for RSC), and Clerk's lower barrier to getting a working flow in production (better quickstart docs, more active Next.js community).
Supabase Auth was also considered. It is a strong option if the team later adopts Supabase as the PostgreSQL provider. Since Urza's Wallet uses Railway-managed PostgreSQL, adding Supabase solely for auth would be a mixed-provider setup that adds complexity.
Custom session auth (e.g., iron-session + jose) was rejected. As the NerdWallet reference architecture notes: "Rolling custom session management without OIDC (OpenID Connect) is an anti-pattern. Non-standard session handling is a common source of security vulnerabilities." Building production-grade session management (rotation, revocation, MFA, breach detection) is a multi-week engineering effort that is not differentiated work for Urza's Wallet.
References
- Richards & Ford, Fundamentals of Software Architecture, Ch. 2 (Trade-offs)
- NerdWallet reference architecture — ADR-004 (CIAM buy vs build)
- Clerk documentation: Next.js App Router quickstart, Admin SDK, Webhooks
- Ley 8968, Article 48 (breach notification); Article 9 (data subject rights)
- Architecture reference §7 (User Profile Architecture), §11 (Security Architecture)