Strategic Direction: Cloud Functions โ Cloud Run What stays as Cloud Functions (permanently) Only event-driven triggers โ nothing else:
cleanupExpiredLanterns โ onSchedule trigger onFrenWrite โ Firestore onDocumentWritten trigger createFeatureRequest/checkDuplicates โ low-traffic, no bugs, acceptable to defer Everything else moves.
Migration Phases Phase 1 โ New services/api/auth (fixes the immediate bugs) The biggest, most urgent chunk. Everything identity/auth-related consolidates here:
Endpoint What it replaces Bug it fixes POST /auth/phone/lookup lookupPhoneUser CF Bug 1 (+ adds real rate limiting) POST /auth/phone/token (missing) Bug 3 (Firebase Auth session) POST /auth/admin/signin signInAdmin CF โ GET /auth/admin/status checkAdminPasswordStatus CF โ POST /auth/admin/password setAdminPassword CF โ POST /auth/roles/:userId setUserRole CF โ POST /auth/moderation/ban banUser CF โ POST /auth/admin/users createAdminUser CF โ POST /auth/phone/reclaim initiatePhoneReclaim CF โ POST /auth/email/encrypt encryptUserEmail CF โ GET /auth/encryption/status checkEncryptionCorruption CF โ The security model for POST /auth/phone/token (key decision):
The plan proposes storing an authProofHash at signup โ specifically HMAC-SHA256(entropy, "lantern-auth-proof-v1"). When a client successfully decrypts the seed with their PIN (proving PIN knowledge locally), they send this HMAC to the server. The server can verify it without ever knowing the PIN or entropy. This is a real zero-knowledge proof layer โ it means someone who just knows a phone number cannot get a custom token. Worth implementing from the start rather than a weaker version.
Also fixes the lockout bypass: server-side attempt tracking in Firestore per userId, with the existing applyServerLockout() client hook already wired in and waiting.
Bugs 1 & 2 are tiny code fixes that don't need the auth service at all โ we can ship those immediately as a patch PR (store phone field in migration, guard signIn() when salt is missing). Bug 3 requires the auth service.
Phase 2 โ Geocoding โ venues service reverseGeocode and enrichVenueAddress move to POST /venues/geocode/reverse and POST /venues/geocode/enrich. Natural fit since the venues service already rate-limits Nominatim calls.
Phase 3 โ Analytics CFs โ analytics service trackUserSignup, trackUserSignin, logClientError fold into the existing analytics service. These call forge.track() the same way the service already does โ straightforward port.
Phase 4 โ New services/api/admin Admin-portal-only functions (system health, billing, ALL GitHub functions) go here. Critically, this service gets ADMIN_ORIGINS CORS only โ the main PWA can never call it. This means GitHub tokens, Cloudflare API keys, and Railway API keys are in a service that ourlantern.app can't reach at all. That's meaningful defense-in-depth.
Client Migration Strategy Add VITE_AUTH_API_URL to the env config. When set, client calls the Cloud Run endpoint. When null, falls back to the CF httpsCallable. This lets you:
Test against the new service in dev first Promote to prod by setting the Cloudflare Pages env var Dual-run briefly, compare error rates Remove CF exports from main.js after validation One thing to confirm before building The plan flags a discrepancy: phoneLookup.js queries where('phone', '==', normalized) but phoneRecycling.js queries where('phoneNumber', '==', normalized). One is wrong. Need to verify which field name the signup actually writes before building the lookup endpoint โ otherwise we just reproduce Bug 1 in the new service.
Ready to start implementing? I'd suggest this order:
Immediate patch PR: Bug 1 (add phone field to migration) + Bug 2 (guard missing salt in signIn()) โ unblocks the pilot now Auth service PR: New services/api/auth with phone lookup + token issuance โ fixes Bug 3 and establishes the foundation