ADR-0003: Authentication via Firebase¶
Status¶
Accepted
Note (2026-07-07): the user-facing sign-in method is passwordless (email-link / magic link), not email/password; social logins (Apple/Google) may be added later. That's a light Firebase/mobile-side choice — the Firebase architecture and backend token validation in this ADR are unchanged, so no separate ADR. The password-reset flow drops out.
Note (2026-07-08, implementation): the first auth sprint resolved this ADR's open points as follows.
- The Firebase ID token is the credential directly — the backend does not mint its own session JWT, so no Redis denylist. The token-validation seam is one DRF authentication class (
interfaces/api/authentication.py), replaceable if this changes.- Account creation is just-in-time in the authentication backend: the first request with a valid token provisions the local user (uid match → verified-email link → create). There is no finalize/registration endpoint — this deliberately improves on
copa-backend, whose separatePOST /auth/sso/finalizestep every client had to remember. Creation and email→uid linking requireemail_verified(account-takeover guard).- Account deletion uses the caller's own ID token against Identity Toolkit
accounts:delete, not thefirebase-adminSDK — no service-account secret to provision, no god-mode credential at rest. Firebase identity is deleted first; if that fails the local account is kept and the call is retryable.- Dev-login escape hatch (
POST /api/auth/dev-login/,DEV_AUTH_ENABLED, off in prod): mints a 24 h HS256 bearer token for any email and creates the account if missing, so QA/tooling can test user endpoints — including fresh-account flows — in environments where a Firebase token can't be obtained.
Date¶
2026-07-01
Context¶
Accounts ship in V1 (see ADR-0001; auth-scope Slack thread, 2026-06-29) so users can persist reflections and their Soul Map. ADR-0001's original plan put identity inside Django via django-allauth (headless) with email/password + Apple/Google + a JWT strategy, coupled to the (then-assumed) Django Ninja API layer.
On the 2026-07-01 stand-up the team decided to move identity out of Django to Firebase Authentication, email/password only. This ADR records that decision; ADR-0001's Auth section now points here.
Decision Drivers¶
- Must: accounts, email/password, password reset, App-Store-mandated account deletion; secure by default; independent of the still-open Django Ninja vs DRF API-framework choice.
- Should: minimise Django-side auth surface to build and maintain inside the ~14-week window; reuse Fueled's proven patterns.
- Nice-to-have: cheap at MVP; addable Apple/Google sign-in later without rework.
Considered Options¶
Option A: django-allauth headless + JWT (ADR-0001's original plan)¶
- Pros: one service, one auth boundary; no external identity dependency; mature Django package covering the whole flow set.
- Cons: full sign-in UX (email/password, reset, social) built and maintained on the Django side; the pick was coupled to Django Ninja (now superseded by DRF — ADR-0004); more surface for the small team to own.
Option B: Firebase Authentication, email/password only — chosen¶
- Pros: Flutter owns the sign-in UX via the Firebase client SDK; the backend only validates the Firebase ID token (RS256 against Firebase JWKS) and maps it to a local user — no passwords, no sign-in UI, no reset flows on our side. Framework-agnostic, so it doesn't wait on the Ninja-vs-DRF decision. Reference implementation exists in Fueled's
copa-backend. Email/password tier is free with no per-user cap. - Cons: identity now depends on an external service with usage-based pricing at the paid tiers; Apple/Google sign-in deferred (email/password only at V1).
Decision¶
We will use Option B: Firebase Authentication, email/password only.
- Flutter handles sign-in via the Firebase client SDK.
- The backend validates the Firebase ID token (RS256, cached JWKS), maps the
subclaim to a localUserrow (created on first sight), and attaches it to the request. A dev-login HS256 escape hatch (percopa-backend) keeps local development offline-friendly. - If the backend mints its own session/JWT after validating Firebase's, refresh-token revocation / denylist lives in Redis; otherwise the Firebase token is the credential directly (decide in the first auth sprint).
- Apple/Google sign-in is deferred — addable later without rework.
- Account deletion stays a V1 deliverable against the local user record (App Store mandate).
Consequences¶
Positive¶
- No Django-side auth-UX surface to build or maintain; sign-in owned by Flutter.
- Independent of the open Django Ninja vs DRF API-framework choice.
- ~$0 auth cost at MVP (Firebase email/password is free with no per-user cap).
Negative¶
- Identity depends on an external service (Firebase) — a new runtime dependency and, at scale, a usage-based cost line.
- Email/password only at V1; adding social providers is future work.
Risks & Mitigations¶
- Risk: Firebase cost grows at scale (flagged by Bhavya) → Mitigation: the free email/password tier has no per-user cap, so risk only appears with paid Identity Platform features or added providers; monitor, don't budget for it yet. Document the cost + architecture on the client-facing Notion architecture page.
- Risk: outage or lock-in on an external identity provider → Mitigation: the backend owns the local
Userrecord keyed by Firebasesub; token validation is a thin, replaceable seam.
Related¶
- ADR-0001 — backend architecture; its Auth section points here.
- Auth-scope Slack thread (2026-06-29) — accounts in V1.
- Stand-up 2026-07-01 — where this decision was made.
- Reference implementation: Fueled's
copa-backend(Firebase JWKS validation + dev-login escape hatch). - Universal Links / App Links backend hosting (CSL-55) —
interfaces/webserves the app-association files +/finishSignInlanding so the passwordless magic-link opens the app.