Admin Auth Provider Linking Plan β
Date: 2026-03-20 Updated: 2026-03-22 Status: Draft Branch: TBD Related issues: #245 (admin password decoupling), #254 (Cloudflare Access removal)
Problem β
When an admin signs up in the main app with phone+PIN, a second Firebase Auth account is created. The claimAdminRole migration attempts to bridge the two accounts by transferring claims, copying data, and disabling the old account. This causes:
- Race condition:
onAuthStateChangedfires before profile/claims exist β access denied - Duplicate profiles: Two
usersdocs, twoadminProfilesdocs for the same person - Disabled accounts: Old email/password Auth account disabled, blocking the email in Firebase
- Fragile migration: Multi-step process with many failure points
Solution β
Provider linking: Instead of creating a second Firebase Auth account, link the phone provider to the existing email/password account. One UID, two providers, no migration.
Target Architecture β
Firebase Auth (single account per admin):
UID: abc123
email: admin@example.com
password: [hashed]
phone: +16195551234
providers: ["password", "phone"]
customClaims: { role: "admin" }Firestore (same UID everywhere):
adminProfiles/abc123 β admin portal data (displayName, adminPasswordHash, etc.)
users/abc123 β main app data (lanternName, encryptedSeed, phone+PIN, etc.)Key Principle β
All new auth endpoints MUST go through services/api/auth/. No new Cloud Functions for auth. Existing Cloud Function duplicates (services/functions/firebase/modules/adminClaim.js, etc.) remain as-is for backward compatibility but are NOT the target for new work.
Simplified Overview Flow β
Admin Signup Flow ( General ) β
Admin Portal β
- Super admin adds admin to the system
- The admin is sent an email link to set up their account in the admin portal
- Admin clicks the link and is directed to the admin portal setup page
- Admin enters their details and creates a password
- Admin submits the form and their account is created
- Admin can now log in to the admin portal using their email and password
Main App ( Admin User ) β
- Super admin adds admin to the system
- The admin is sent a text message with a dedicated admin link to set up their account in the main app ( or the super admin can text the admin the link directly ) -- Would be efficient to have their number pre-filled in the link so they don't have to enter it again
- Admin clicks the link and is directed to the main app setup page
- Admin enters their details and creates a password
- Admin submits the form and their account is created
- Admin can now log in to the main app using their phone + pin
Edge Cases β
Admin Number is Already in Use by another User ( admin or regular user ) β
- If the admin's phone is already associated, we can fallback to the email flow for the main app setup
- OR the admin goes through the flow to claim their number and then can proceed with the main app setup
The admin is a current user but not an admin β
- If the admin's phone is already associated with a regular user account that belongs to them, we can allow them to claim their number and then proceed with the main app setup
Comprehensive Flows β
Flow 0: Admin Created in Portal (Invite via SMS) β
When a super admin creates a new admin in the admin portal:
1. Super admin fills out admin creation form:
- Email (for portal login)
- Display name
- Phone number (NEW β required field)
2. Admin portal creates:
- Firebase Auth account (email/password)
- adminProfiles/{uid} with phone number
- Custom claims: { role: "admin" }
3. Server generates invite link and stores token:
- Invite token is a one-time-use token stored in Firestore (adminInvites collection)
- Token contains: admin UID, email, expiry (e.g., 7 days)
- Link format: https://[dev.]ourlantern.app/#/admin-signup?token=<invite-token>
- Admin portal displays the link for super admin to copy and send manually
- (Future: automate via Twilio or similar SMS provider)
4. Super admin sends link to new admin (text, email, Slack, etc.)
5. Admin taps link β opens main app at #/admin-signup routeFlow 1: Admin Signup in Main App (Dedicated URL β #/admin-signup) β
Admin arrives via invite link (#/admin-signup?token=<invite-token>):
Step 1: Validate invite token
β POST /auth/admin/invite/validate
- Input: { token }
- Server: Looks up token in adminInvites, verifies not expired/used
- Returns: { valid: true, email: "admin@example.com", uid: "abc123" }
β Client shows: "Welcome to Lantern! You've been invited as an admin."
Step 2: Phone number + age verification
β Phone number pre-filled from adminProfiles if available
Step 3: SMS verification (provider linking)
β POST /auth/admin/provider-link/start
- Input: { token } (server already knows email/uid from token)
- Server: Generates custom token for the admin's existing UID
- Returns: { customToken, uid }
β Client: signInWithCustomToken(customToken)
- Now authenticated as the EXISTING admin UID
β Client: linkWithPhoneNumber(phoneNumber, recaptchaVerifier)
- Sends SMS, returns confirmationResult
β Client: confirmationResult.confirm(smsCode)
- Links phone provider to existing account
Step 4: PIN creation (unchanged)
Step 5: Account creation
β createAccount() writes users/{existingUID} (not a new UID)
β No claimAdminRole needed β UID already has admin claims
β Email already known from invite β no email capture step needed
Step 6: Recovery phrase + terms (unchanged)Flow 2: Admin Signup Fallback (Phone Already Exists on Another Account) β
If linkWithPhoneNumber() fails with auth/credential-already-in-use:
β Existing claimAdminRole migration runs (current behavior)
β User sees: "This phone is already linked to another account.
Falling back to account migration..."
β This is the safety net β no behavior change from todayFlow 3: Regular User Signup (#/signup) β
β Completely unchanged
β Normal phone+PIN signup creates a new Firebase Auth account
β No admin checks, no provider linking
β Works in both pilot and non-pilot modeFlow 4: Pilot Mode Access (Non-Admin Blocked) β
β PILOT_MODE gate in App.jsx unchanged
β Regular users who sign up via #/signup get AccessGateDenied (pilot only)
β Admins who sign up via #/admin-signup have claims from the start β gate passes
β No Step 0 email gate needed in regular signup flow anymoreFlow 5: Admin Deletes Lantern Account β
β Delete users/{uid} Firestore doc
β Remove phone provider from Firebase Auth account
β Clear custom claims related to main app (if any)
β DO NOT delete Firebase Auth account (email/password provider remains)
β DO NOT delete adminProfiles/{uid}
β Admin portal continues to workFlow 6: Admin Removed from Portal β
β Delete adminProfiles/{uid}
β Remove admin custom claims
β Update users/{uid}.role from "admin" to "user" (if users doc exists)
β DO NOT delete Firebase Auth account (phone provider remains)
β DO NOT delete users/{uid}
β Main app continues to work (as regular user)Flow 7: Admin Deletes Everything β
β Must explicitly delete from BOTH systems
β Or: admin portal deletion + Lantern account deletion in sequence
β When last provider is removed, Firebase Auth account is deletedNew API Endpoints β
All new endpoints go in services/api/auth/. No new Cloud Functions.
POST /auth/admin/invite/create β
Location: services/api/auth/src/routes/adminInvite.js
Auth: Firebase token + admin role required
Purpose: Called by admin portal after creating a new admin. Generates invite link for manual sharing.
Request:
{
"adminUid": "abc123"
}Process:
- Verify caller is admin
- Verify target UID exists and has admin claims
- Generate one-time invite token (crypto.randomUUID or similar)
- Store in Firestore
adminInvites/{token}:adminUid,email,phone,createdBy,createdAt,expiresAt(7 days),used: false
- Build invite URL based on environment
- Return link for super admin to share manually
Response:
{
"success": true,
"inviteId": "token-uuid",
"inviteUrl": "https://dev.ourlantern.app/#/admin-signup?token=token-uuid"
}POST /auth/admin/invite/validate β
Location: services/api/auth/src/routes/adminInvite.js
Auth: App Check + rate limiting (no Firebase token β user isn't signed in yet)
Purpose: Client validates the invite token when admin taps the link.
Request:
{
"token": "invite-token-uuid"
}Process:
- Look up
adminInvites/{token} - Verify: exists, not expired, not used
- Return admin email (for display) and confirmation
Response (success):
{
"valid": true,
"email": "admin@example.com",
"displayName": "mechelle"
}Response (invalid/expired):
{
"valid": false,
"reason": "expired" | "already_used" | "not_found"
}Security:
- Rate limited: 10 req/15min per IP
- Does NOT return UID or custom token at this stage
- Token is single-use β marked
used: trueafter successful signup
POST /auth/admin/provider-link/start β
Location: services/api/auth/src/routes/adminProviderLink.js
Auth: App Check + rate limiting (no Firebase token β user isn't signed in yet)
Purpose: After invite validated and phone entered, issues a custom token so the client can sign into the existing admin account and link the phone provider.
Request:
{
"token": "invite-token-uuid"
}Process:
- Look up
adminInvites/{token}β verify valid and not yet used - Get admin UID from invite
- Look up Firebase Auth user by UID
- Verify they have admin claims (
role: 'admin') - Verify they do NOT already have a phone provider linked
- Generate custom token:
auth.createCustomToken(uid) - Return token to client
Response (success):
{
"success": true,
"customToken": "eyJ...",
"uid": "abc123"
}Response (already has phone):
{
"success": false,
"reason": "phone_already_linked"
}Security considerations:
- Rate limited: 5 req/15min per IP
- Requires App Check verification
- Only returns custom token for verified admin accounts via valid invite token
- Custom tokens expire in 1 hour
- Invite token is NOT consumed here β consumed after successful signup completion
- Does NOT accept email directly β only works via invite token (prevents enumeration)
Files to Modify β
New files: β
| File | Purpose |
|---|---|
services/api/auth/src/routes/adminInvite.js | Invite endpoints: create invite link, validate token |
services/api/auth/src/routes/adminProviderLink.js | Provider linking: issue custom token for phone linking |
services/api/auth/src/routes/adminEncryption.js | POST /auth/admin/encryption/check β migrated from checkEncryptionCorruption Cloud Function |
apps/web/src/screens/auth/AdminSignup.jsx | Dedicated admin signup flow at #/admin-signup |
apps/admin/src/lib/authApi.js | API client helper β centralized fetch() calls with auth token headers, replaces httpsCallable() pattern |
Modified files: β
| File | Change |
|---|---|
services/api/auth/src/index.js | Register new invite + provider-link routes |
apps/web/src/App.jsx | Add #/admin-signup route, add handleSignupComplete refresh for admin auth state |
apps/web/src/lib/profileService.js | Include phoneSalt in getPublicProfile (verify current state β may already be present) |
apps/admin/src/components/CreateAdminForm.jsx | Add phone number as required field |
services/api/auth/src/routes/adminUsers.js | POST: store phone on adminProfiles, call /auth/admin/invite/create to generate invite link. DELETE: remove phone provider instead of deleting Auth account. Note: Admin deletion currently lives in Cloud Functions (services/functions/firebase/modules/adminUsers.js); the API route may need to be created or the Cloud Function updated depending on implementation approach. |
firestore.rules | Add adminInvites collection rules (server-write only, no client access) |
apps/admin/src/firebase.js | Phase 6: Replace all httpsCallable() admin auth calls with API client calls. Remove Cloud Function imports when no longer needed. |
services/functions/firebase/main.js | Phase 6: Remove all admin auth/claim/users/deletion exports (lines 8-11, 32-40) |
NOT modified (during Phases 1β4): β
| File | Reason |
|---|---|
services/api/auth/src/routes/adminClaim.js | Fallback for provider conflict cases β deprecated in Phase 5 |
services/api/auth/src/routes/adminAuth.js | Existing admin password endpoints (/signin, /password, /password/reset, /status) β unrelated to invite flow |
apps/web/src/screens/auth/PhonePinSignup.jsx | Regular user signup β unchanged, no admin logic |
Deleted (Phase 6 β Cloud Function consolidation): β
| File | Action |
|---|---|
services/functions/firebase/modules/adminAuth.js | DELETE. All 6 functions duplicated in API (signInAdmin, setAdminPassword, requestAdminPasswordReset, verifyAdminResetToken, checkAdminPasswordStatus). checkEncryptionCorruption migrated to API in Phase 6. |
services/functions/firebase/modules/adminClaim.js | DELETE. claimAdminRole duplicated at POST /auth/admin/claim-role in API. Deprecated in Phase 5, deleted in Phase 6. |
services/functions/firebase/modules/adminUsers.js | DELETE. createAdminUser and resendAdminSetupLink duplicated in API. |
services/functions/firebase/modules/adminDeletion.js | DELETE. deleteAdminUser duplicated at DELETE /auth/admin/users/:userId in API. |
services/functions/firebase/main.js | MODIFY (not delete). Remove exports for the 4 deleted modules (lines 8-11, 32-40). All other exports remain. |
apps/admin/src/firebase.js | MODIFY (not delete). Remove admin auth httpsCallable() calls, replace with API client imports. Keep all other httpsCallable() calls (~20+ for GitHub, billing, moderation, etc.). Firebase Functions SDK import must remain. |
Edge Cases β
Invite system β
| Scenario | Handling |
|---|---|
| Admin taps expired invite link | Show "This invite has expired. Contact your admin for a new one." |
| Admin taps already-used invite link | Show "This invite has already been used. Try logging in instead." |
| Invite token brute-forced | Rate limiting (10 req/15min per IP) + UUIDs are unguessable |
| SMS fails to send | Return error to admin portal, allow resend |
| Admin loses the SMS / link | Admin portal has "Resend invite" button |
Provider conflicts β
| Scenario | Handling |
|---|---|
| Phone already on another Firebase Auth account | Fall back to existing claimAdminRole migration |
| Admin already has phone linked (re-signup) | Return phone_already_linked, guide to login instead |
| Custom token expires before phone linking | Client retries POST /auth/admin/provider-link/start with same invite token |
| linkWithPhoneNumber fails mid-SMS | User can retry SMS verification (standard Firebase behavior) |
Account lifecycle β
| Scenario | Handling |
|---|---|
| Admin deletes Lantern account | Remove phone provider + users doc. Keep Auth account + adminProfiles |
| Admin removed from portal | Delete adminProfiles + revoke claims. Keep Auth account + users doc (demoted to user) |
| Admin deletes both | Each deletion independent. Last provider removal β Auth account cleanup |
| Phone number changes | Unlink old phone, link new phone (future feature, not in scope) |
Auth state β
| Scenario | Handling |
|---|---|
| signInWithCustomToken triggers onAuthStateChanged before profile exists | handleSignupComplete must refresh admin status + profile after account creation completes |
| Token refresh after linkWithPhoneNumber | Force getIdToken(true) to pick up new provider |
| Admin claims not propagated after linking | Force token refresh in createAccount() |
Deployment (Phase 6) β
| Scenario | Handling |
|---|---|
| Auth API deploys but admin portal deploy fails | No impact β admin portal still uses old httpsCallable() calls against existing Cloud Functions |
| Admin portal deploys but auth API deploy fails | Breaking β admin portal calls API endpoints that don't exist. Mitigate by deploying auth API first and verifying health before admin portal deploys |
| Cloud Functions removed before admin portal switches to API | Breaking β httpsCallable() calls fail with "function not found". Must deploy in order: API β admin portal β Cloud Functions |
Pilot mode interaction β
| Scenario | Handling |
|---|---|
| Non-admin visits #/admin-signup without token | Show error: "You need an invite link to sign up as an admin." |
| Non-admin visits #/admin-signup with invalid token | Show error via invite/validate response |
| Admin visits #/signup instead of #/admin-signup | Normal signup flow, no provider linking. If pilot mode, gets AccessGateDenied. Must use correct URL. |
| Pilot mode disabled later | #/admin-signup still works for admin onboarding. #/signup opens to all users. |
Testing Plan β
Unit tests: β
- [ ] POST /auth/admin/invite/create: creates invite, stores in Firestore, returns URL
- [ ] POST /auth/admin/invite/validate: valid token β returns admin info
- [ ] POST /auth/admin/invite/validate: expired token β returns expired
- [ ] POST /auth/admin/invite/validate: used token β returns already_used
- [ ] POST /auth/admin/provider-link/start: valid invite β returns custom token
- [ ] POST /auth/admin/provider-link/start: admin with phone already linked β returns phone_already_linked
- [ ] POST /auth/admin/provider-link/start: invalid invite β generic rejection
- [ ] Rate limiting enforced on all unauthenticated endpoints
- [ ] profileService.getPublicProfile includes phoneSalt
Integration tests (manual, dev environment): β
- [ ] Full happy path: admin created in portal β invite link generated β admin taps link β validates β provider linked β account created β admin access works in main app
- [ ] Fallback: phone already on another account β claimAdminRole migration runs β admin access works
- [ ] Expired invite β clear error, admin portal can resend
- [ ] Delete Lantern account β admin portal still works (email/password provider remains)
- [ ] Remove from admin portal β Lantern account still works as regular user (phone provider remains)
- [ ] Re-signup after Lantern deletion β new invite β provider linking works again
- [ ] Regular user signup via #/signup β unchanged behavior, no admin logic
- [ ] Visit #/admin-signup without token β clear error message
- [ ] Existing admin with linked phone β guided to login instead of re-signup
Cloud Function migration tests (Phase 6): β
- [ ] POST /auth/admin/encryption/check: valid passphrase + canary β returns not corrupted
- [ ] POST /auth/admin/encryption/check: wrong passphrase β returns corrupted
- [ ] Admin portal sign-in works via API (
POST /auth/admin/signin) instead of Cloud Function - [ ] Admin password set/reset works via API instead of Cloud Function
- [ ] Admin creation works via API (
POST /auth/admin/users) instead of Cloud Function - [ ] Admin deletion works via API (
DELETE /auth/admin/users/:userId) instead of Cloud Function - [ ] Resend setup link works via API instead of Cloud Function
- [ ] No remaining
httpsCallablereferences to deleted admin functions - [ ] Cloud Functions deploy succeeds with admin exports removed
- [ ] No orphaned Cloud Function references in codebase (grep for deleted function names)
Regression tests: β
- [ ] Passphrase login still works
- [ ] Phone+PIN login still works for existing accounts
- [ ] Admin portal login unaffected (now via API, not Cloud Functions)
- [ ] PhoneMigrationFlow (passphrase β phone+PIN) still works
- [ ] Merchant creation unaffected
- [ ] Existing claimAdminRole fallback still works end-to-end (until Phase 5 deprecation)
Prerequisites β
Before starting any phase, these must be in place:
Add
VITE_AUTH_API_URLto admin portal deployment config. The admin portal currently has no auth API URL configured. The main app usesVITE_AUTH_API_URL=/api(Cloudflare rewrites to Cloud Run). Add the same pattern for admin:.github/workflows/deploy-dev.ymlβ admin portal build step (addVITE_AUTH_API_URL: /api).github/workflows/deploy-prod.ymlβ same.env.local.exampleβ document for local dev:VITE_AUTH_API_URL=http://localhost:8084
Verify CORS allowlist includes admin portal origins. Already confirmed β
services/api/auth/src/index.jsallowsadmin.dev.ourlantern.app,admin.ourlantern.app, andlocalhost:3001/5174. No changes needed.Verify App Check works from admin portal to API. The admin portal initializes App Check (ReCaptchaV3), but currently only uses it for Cloud Functions (automatic via Firebase SDK). The API client helper must fetch an App Check token and pass it as the
X-Firebase-AppCheckheader on unauthenticated endpoints (/status,/password/reset).
Rollout β
Phase 1: Backend β Invite system + provider linking β
- Add phone number as required field in admin creation (admin portal + API)
- Create
adminInvitesFirestore collection + security rules - Create
POST /auth/admin/invite/createendpoint - Create
POST /auth/admin/invite/validateendpoint - Create
POST /auth/admin/provider-link/startendpoint - Add unit tests for all new endpoints
- Deploy to dev
Phase 2: Frontend β Admin signup flow β
- Create
AdminSignup.jsxcomponent for#/admin-signuproute - Add route to App.jsx (add to
authRoutesarray so access gate is bypassed) - Wire up: validate invite β enter phone β provider link β PIN β account creation
- Test full flow on dev with test admin accounts
Phase 3: Account lifecycle β
- Update admin deletion (portal side): remove admin claims, keep phone provider
- Update Lantern account deletion: remove phone provider, keep email/password
- Test deletion flows in both directions
- Verify no cross-contamination
Phase 4: Admin portal integration β
- Add "Send Invite" / "Resend Invite" buttons to admin management UI
- Show invite status on admin profiles (pending, accepted, expired)
- Backfill phone numbers on existing admin profiles (manual)
Phase 5: Cleanup β Deprecate legacy flows β
- Remove pilot mode Step 0 (admin email gate) from PhonePinSignup
- Deprecate
POST /auth/admin/verify-emailendpoint (only used by Step 0 email gate) - Deprecate
POST /auth/admin/claim-roleAPI endpoint (replaced by provider linking) - Monitor fallback claimAdminRole usage before removing
- Remove deprecated API endpoints when no longer triggered
Phase 6: Cloud Function consolidation β migrate all callers to API, then delete β
The admin portal (apps/admin/src/firebase.js) currently calls every admin auth operation via httpsCallable() (Cloud Functions). 9 of 10 Cloud Functions already have identical API equivalents. This phase migrates all callers to the API and deletes the Cloud Functions.
Step 1: Migrate checkEncryptionCorruption to the API (CF-only, no API equivalent yet) 24. Create POST /auth/admin/encryption/check endpoint in services/api/auth/src/routes/adminEncryption.js 25. Port checkEncryptionCorruption logic (server-side PBKDF2 + AES-GCM canary decryption) 26. Register route in services/api/auth/src/index.js 27. Add unit tests
Step 2: Switch admin portal callers from Cloud Functions to API
All callers are in apps/admin/src/firebase.js via httpsCallable(). Replace each with a fetch() call to the equivalent API endpoint:
Current httpsCallable() call | Replace with API call |
|---|---|
httpsCallable(functions, 'signInAdmin') | POST /auth/admin/signin |
httpsCallable(functions, 'setAdminPassword') | POST /auth/admin/password |
httpsCallable(functions, 'requestAdminPasswordReset') | POST /auth/admin/password/reset |
httpsCallable(functions, 'verifyAdminResetToken') | GET /auth/admin/password/reset/:token |
httpsCallable(functions, 'checkAdminPasswordStatus') | GET /auth/admin/status |
httpsCallable(functions, 'checkEncryptionCorruption') | POST /auth/admin/encryption/check (new, from Step 1) |
httpsCallable(functions, 'createAdminUser') | POST /auth/admin/users |
httpsCallable(functions, 'resendAdminSetupLink') | POST /auth/admin/users/:userId/resend-setup |
httpsCallable(functions, 'deleteAdminUser') | DELETE /auth/admin/users/:userId |
- Create an API client helper in admin portal (
apps/admin/src/lib/authApi.js) to centralizefetch()calls. Must handle:- Firebase auth token in
Authorization: Bearer <token>header (for authenticated endpoints) - App Check token in
X-Firebase-AppCheckheader (for unauthenticated endpoints) - Error handling translation:
httpsCallablethrows Firebase errors witherror.code(e.g.,functions/failed-precondition) anderror.message. API returns HTTP status codes (401, 403, 428, etc.) with JSON bodies. Each caller'scatchblock must be updated.
- Firebase auth token in
- Update each caller function in
apps/admin/src/firebase.jsto use the API client instead ofhttpsCallable() - Verify admin portal components still work β affected files:
apps/admin/src/components/LoginScreen.jsx(requestAdminPasswordReset)apps/admin/src/components/SetAdminPassword.jsx(verifyAdminResetToken, setAdminPassword)apps/admin/src/components/SetupAdminPasswordModal.jsx(setAdminPassword)apps/admin/src/components/CreateAdminForm.jsx(createAdminUser)apps/admin/src/components/UserDetailPanel.jsx(deleteAdminUser, resendAdminSetupLink)
Step 3: Delete Cloud Function modules and remove exports
- Remove exports from
services/functions/firebase/main.js:- Line 8:
export { createAdminUser, resendAdminSetupLink } from './modules/adminUsers.js' - Line 9:
export { claimAdminRole } from './modules/adminClaim.js' - Line 11:
export { deleteAdminUser } from './modules/adminDeletion.js' - Lines 32-40:
export { setAdminPassword, signInAdmin, requestAdminPasswordReset, verifyAdminResetToken, checkAdminPasswordStatus, checkEncryptionCorruption } from './modules/adminAuth.js'
- Line 8:
- Delete the Cloud Function module files:
services/functions/firebase/modules/adminAuth.jsservices/functions/firebase/modules/adminClaim.jsservices/functions/firebase/modules/adminUsers.jsservices/functions/firebase/modules/adminDeletion.js
- Do NOT remove
httpsCallableor Firebase Functions SDK from admin portal. The admin portal still uses ~20+ other Cloud Functions (GitHub, billing, moderation, user roles, system health, etc.) that are NOT being migrated. Only the admin auth subset moves to the API. - Search codebase for any other references to the deleted function names and remove them (check:
apps/web/src/App.jsxhas a comment referencingclaimAdminRole) - Deploy in correct order: Auth API first (so endpoints are live), then admin portal (so callers point to API), then Cloud Functions (with removed exports). If admin deploys before auth API, admin sign-in breaks.
- Verify all admin portal flows still work end-to-end against the API
Open Questions β
- Existing admins who already migrated: Do we clean up the duplicates from the old migration (two UIDs, two profiles), or leave them?
- PhoneMigrationFlow: Does the existing passphraseβphone migration flow need updates to handle the shared UID case?
- Invite resend limit: How many times can an invite be regenerated before it's suspicious?
Invite link format: Deep link vs. regular URL?RESOLVED: Hash-based URLs (https://[dev.]ourlantern.app/#/admin-signup?token=xxx) work when tapped from text messages on iOS/Android. Hash fragments are preserved by browsers and messaging apps. No deep link setup needed β the PWA handles#/routing natively.- Future SMS automation: When ready to automate invite delivery, evaluate Twilio, Firebase Extensions, or similar. For now, super admin copies and sends the link manually.
- Phase 6 deployment ordering: GitHub Actions deploys admin portal and auth API in parallel. For Phase 6, we need auth API deployed and healthy before admin portal goes live. Options: (a) make admin portal deployment depend on auth API job, (b) deploy Phase 6 in two separate PRs (API first, then admin portal + CF cleanup), (c) add runtime fallback in admin portal API client that falls back to
httpsCallable()if API is unreachable. - Migrate remaining Cloud Functions to API services long-term? The admin portal still uses ~20+ Cloud Functions for GitHub integration, billing, moderation, user roles, and system health. Should these eventually move to dedicated API services (e.g.,
services/api/admin/) for full consistency? Not in scope for this plan, but worth tracking.