Skip to content

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 separate POST /auth/sso/finalize step every client had to remember. Creation and email→uid linking require email_verified (account-takeover guard).
  • Account deletion uses the caller's own ID token against Identity Toolkit accounts:delete, not the firebase-admin SDK — 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 sub claim to a local User row (created on first sight), and attaches it to the request. A dev-login HS256 escape hatch (per copa-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 User record keyed by Firebase sub; token validation is a thin, replaceable seam.
  • 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/web serves the app-association files + /finishSignIn landing so the passwordless magic-link opens the app.