4ce2df0a2b
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 14s
Deploy Staging / deploy (push) Successful in 18s
CI / backend (pull_request) Successful in 52s
CI / frontend (pull_request) Successful in 15s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 55s
Security / secrets (pull_request) Successful in 4s
Security / dependencies (pull_request) Successful in 54s
3.3 KiB
3.3 KiB
12 — Migration Guide (Part 10)
Moving the current single-account Gmail app to the multi-provider, multi-user model — additive and reversible, no destructive step. Executed as ordered EF Core migrations + idempotent backfills, behind a short maintenance window.
Principles
- Additive first: create new tables/columns before moving data; keep old columns until parity is verified.
- Idempotent backfills: safe to re-run; keyed on stable ids.
- Flag-gated cutover: the new sign-in/model activates behind flags; the old path stays until removed.
- Reversible: each step has a documented rollback; no data is deleted during migration.
Step-by-step
- Schema (additive migration)
- Create
users,accounts,provider_tokens,sessions,user_settings,system_settings,feature_flags,audit_logs. - Add
account_id,user_id(nullable) to the current email/thread tables; widensearch_vector; add nullableembedding vector(768)+pgvectorextension.
- Create
- Identity backfill
- For the existing operator/user, create a
usersrow; mark the first user = Admin. - Create one
Googleaccountper existing identity (is_login_identity=true); move current encrypted Gmail tokens →provider_tokens.
- For the existing operator/user, create a
- Email backfill
- Set
account_id/user_idon all existingEmail/thread rows to the default Google account. - Regenerate the widened
search_vector; leaveembeddingnull (backfilled later by the AI phase). - Enforce the new unique keys
(account_id, provider_message_id)/(account_id, provider_thread_id).
- Set
- Config seed
- Seed
feature_flags:provider.google=on,provider.microsoft=off,provider.imap=off,ai.enabled= derived from the currentAi:Mode(Disabled→off),ai.*=off,maintenance.*=off. - Create
system_settingssingleton; createuser_settingsfrom any existing per-user prefs (else defaults).
- Seed
- Cutover
- Enable the new OAuth-as-login + unified sync behind their flags; verify on staging first (the pipeline we built), then production via a tagged release.
- Cleanup (later, separate migration)
- Once parity is confirmed in production, drop obsolete columns/paths. Not part of the cutover.
Verification checklist
- Existing user signs in via Google → lands on their mail unchanged.
- Email counts match pre/post; search returns identical results for sample queries.
- Tokens decrypt and refresh; sync resumes from the correct cursor.
- No cross-user rows visible (isolation test).
Rollback
- Pre-cutover: additive changes are inert → simply don't flip the flags; drop new tables if aborting.
- Post-cutover issue: flip flags off / redeploy previous tag; old columns still present → the legacy path still works. No data was deleted, so no data rollback is needed.
Provider-app prerequisites (operator setup)
- Google: OAuth client (existing) + redirect
/(…)/signin/google; scopesgmail.readonly gmail.modify. - Microsoft: register an Entra app; redirect
/(…)/signin/microsoft; scopesMail.Read Mail.ReadWrite offline_access; admin consent if required. - IMAP (future): per-account host/credentials; validated against the SSRF allowlist.
- Because providers are flag-gated, an unconfigured provider is simply hidden — configure, then enable.