Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
6.9 KiB
Migration readiness — MySQL → Postgres + post-import passwords
Last updated: 2026-08-04
Live ETL status: DONE on staging (dry-run + load, 2026-08-03) — see migration-run-log.md.
Production cutover: NOT ready — remaining blockers below.
Migrated users are imported with password_hash = NULL and must_set_password = true. Login rejects empty/null hashes, so a random guess cannot succeed.
Fresh installs that never ran the migrator can keep using /register.
Live dry-run + load (staging)
| Gate | Status |
|---|---|
Goose 001–007 on staging PG (localhost:5433) at ETL time (2026-08-03) |
Done — repo schema is now 001–023; re-apply goose up before any new staging/prod load |
| Offline fixture dry-run | Done |
| Live MySQL dry-run | Done (exit 0) |
| Live load to staging Postgres | Done (~11 min) |
| Orphan FK validation | 16/16 pass |
| Evidence | migration-run-log.md; artifacts/ gitignored |
Counts written (staging)
| Entity | Written |
|---|---|
| companies | 27 |
| users | 21 |
| memberships | 15 |
| categories | 8278 |
| attributes | 57630 (unique (company_id, attribute_key) collapse) |
| raw_products | 118784 (+7732 GTIN-deduped from MySQL) |
| processed_products | 7219 |
| input feeds | 31 |
| export feeds | 8 |
Remaining blockers (before production cutover)
| Severity | Item |
|---|---|
| Data | Most users have synthetic …@legacy.local emails — export from Clerk and patch users.email before invites (tooling: migrator -list-legacy-emails / -export-legacy-emails / -patch-emails; see portable-mysql-pg-migration.md) |
| Data | All memberships imported as role=member — promote company admins |
| Data | 2 company_plans skipped (plan_id=6 missing) — resolve with migrator list → -dry-run → -confirm (see cutover.md; no blind live assigns) |
| Ops | This load used -skip-post-import — re-issue set-password invites when emails are real |
| Ops | SMTP + mailhooks + login smoke still unproven |
| Gaps | See Cutover coverage gaps below |
Staging is GO for login testing after set-password (preferably after email patch). Production DNS/cutover remains NO-GO.
Cutover coverage gaps
Honest status vs migrator code (cmd/migrator, domains settings / woo / files):
| Area | Status | Operator action |
|---|---|---|
| API keys | Not migrated (intentional — secrets) | Clients must mint new keys; Settings → API Keys empty state says so |
| company_settings | Partial — language + merge_products only (migrateCompanySettings) |
Re-enter other legacy settings in-app if needed |
| File blobs | Metadata only — bytes not copied (migrateFiles) |
Resync object storage / re-upload; Files empty + dashboard ETL gaps panel say so; platform admins see cheap COUNTs on Admin → Diagnostics → ETL gap inventory (files_metadata_only / files_total) |
| Woo configs | Migrated when domain woo enabled — from wc_* custom_fields (migrateWooConfigs) |
Re-verify store URL + credentials; ensure APP_ENCRYPTION_KEY before prod secrets |
| Jobs / history | Cutover default: not backfilled — optional domain jobs can import processing_jobs (+ best-effort products) + tasks tagged ai_provider_mode=migrated; accepted production gap is empty history unless ops explicitly ran jobs |
Expect empty Processing history after cutover; tenant UI must not claim history moved; diagnostics migration_inventory.jobs_domain_ran / processing_jobs_migrated show whether optional backfill ran |
Migrator run reports surface these as notes + count-report rows (api_keys, files metadata-only, processing_jobs when domain enabled, partial company_settings).
Admin inventory (read-only): GET /api/admin/diagnostics → migration_inventory — COUNT of metadata-only files, migrated-tagged jobs, and tasks. Not an import path; never fabricates blob bytes or job history.
After a live import
1. Issue set-password invites (preferred)
Post-import already runs this unless -skip-post-import. To re-issue later (Postgres only, no MySQL):
cd apps/api
export DATABASE_URL="postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable"
# WEB_ORIGIN controls printed /accept-invite links (default http://localhost:5174)
go run ./cmd/migrator -issue-set-password-invites \
-postgres "$DATABASE_URL" \
-maps-dir ../../artifacts
Artifacts (gitignored, do not commit):
artifacts/password_invites.json— email, token, invite URL, expiryartifacts/set-password-hooks.json— same payload forcmd/mailhooks
Stdout prints email<TAB>url for each invite. Open a URL → /accept-invite → set password → session (invite path) or sign in.
No-SMTP staging rehearsal (promote admin → re-issue → copy link → one login): staging-auth-rehearsal.md.
Invite tokens hit POST /api/auth/accept-invite (DB invite row). Existing users get password_hash set and must_set_password cleared.
2. Email delivery (optional)
cd apps/api
# Smoke first (works with EMAIL_DRY_RUN default true; no SMTP dial)
go run ./cmd/mailhooks -hooks ../../artifacts/set-password-hooks.json -dry-run
# Live send requires EMAIL_DRY_RUN=false + SMTP — see ops-runtime.md
go run ./cmd/mailhooks -hooks ../../artifacts/set-password-hooks.json
Operator smoke steps (fail-closed under dry-run, pass criteria): ops-runtime.md § SMTP.
3. Local bootstrap one user (dev only)
cd apps/api
export DATABASE_URL="postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable"
go run ./cmd/migrator -set-password "user@example.com:YourPass123" \
-postgres "$DATABASE_URL"
Then sign in at /login. Do not use this for production cutover.
4. Admin HMAC emails (platform admin UI)
POST /api/admin/emails/set-password emails links with ?mode=set-password. Those tokens are HMAC (TOKEN_SIGNING_SECRET), not DB invites. The accept-invite page calls POST /api/auth/complete-set-password, then redirects to /login.
5. Authenticated set-password
Logged-in users can call POST /api/auth/set-password (session required) — useful after a partial login path, not for first-time migrated users.
Smoke checklist
- Migrated user cannot log in before setting a password.
- Invite URL from
password_invites.jsonopens/accept-invite. - Submit password (≥8 chars) → logged in (invite) or redirected to login (HMAC mode).
- Subsequent
/loginworks with the new password. must_set_passwordis false in Postgres.
See also cutover.md §4, go-live-checklist.md, and ops-runtime.md.