This document turns the migration direction into an execution plan.
The goal is not only to migrate data. The goal is to migrate without breaking user flows, without lowering product quality, and without introducing hidden data loss risk.
When this roadmap is complete:
Supabase Postgresis the only source of truth for product dataSupabase Storageis used only for files and binary assetssqliteis no longer production truthmemoryremains dev/test fallback only- every critical user-facing flow has rollback-safe cutover steps
These rules are non-negotiable.
-
No big-bang migration- every major domain moves behind a feature flag or explicit cutover gate
-
No dual ambiguity- during each phase, one runtime path is primary and one is compatibility-only
-
No migration without verification- each phase needs:
- schema verification
- data backfill verification
- runtime smoke tests
- rollback path
- each phase needs:
-
No product regression- UX and operational behavior must remain unchanged unless intentionally improved
-
Storage stays for files only- JSON state is a temporary compatibility bridge, not an end-state
graph TD
A["Phase 0: Freeze Truth Map"] --> B["Phase 1: Identity + Sessions"]
B --> C["Phase 2: Portfolio Profile"]
C --> D["Phase 3: Certifications + Education"]
D --> E["Phase 4: Reports"]
E --> F["Phase 5: Telemetry Events"]
F --> G["Phase 6: Incidents + Operational Graph"]
G --> H["Phase 7: Cleanup Legacy Stores"]
H --> I["Phase 8: Hardening + Observability"]
Make sure we know exactly what data exists, where it lives, and what “correct” means before we move anything.
Already started.
- data-flow-map-and-migration-plan.md
- platform-backbone-v1.sql
- updated README.md
- every major data domain is mapped
- every target table exists in the SQL backbone draft
- outdated repo summary is removed
Move users and sessions to Postgres first, because identity is the safest and most important first relational backbone.
- login
- register
- session lookup
- session cleanup
- admin user creation
- assignable user listing
Code + schema COMPLETE (2026-05-28); runtime still flag-OFF (zero prod impact):
- email-aware
identity.users+identity.sessionsschema APPLIED to the Supabase Postgres projectcybersec-blog(verified via list_tables) - postgres identity adapter has NO stubs — login, register, session
lookup/cleanup, admin create, assignable list,
readUserByEmailKey(OAuth), email-verify + password-reset +deleteAllSessionsForUserare all real - backfill script carries the email columns (OAuth accounts survive cutover)
- default runtime unchanged (
SOC_IDENTITY_STORE=supabase); postgres dormant
Code + schema are done. What's left touches the live DB + env, so the operator
runs it (the agent never touches .env). Run in order:
- Lock the tables — REQUIRED security step. RLS is disabled and these are
service-role-only tables. BEFORE exposing the schema, run in the Supabase
SQL editor:
No policies needed: the service-role key the adapter uses BYPASSES RLS, so enabling it with zero policies denies the anon key (which must never be able to read password hashes / sessions).
alter table identity.users enable row level security; alter table identity.sessions enable row level security;
- Expose the schema to PostgREST. Dashboard → Settings → API → Exposed
schemas → add
identity. The adapter queries.schema('identity')via PostgREST; without this it can't reach the tables. - Set env (operator-only):
SOC_IDENTITY_STORE=postgres. NoDATABASE_URLneeded — the adapter reusesSUPABASE_URL+SUPABASE_SERVICE_ROLE_KEY. - Backfill users/sessions (carries email):
npm run backfill:identity(dry-run, review) →... -- --apply, then run the printedsetval(...)SQL in the SQL editor (repairs the id sequence). - Cutover + verify under
SOC_IDENTITY_STORE=postgres: password login, GitHub/Google OAuth, register, logout, session restore on refresh, admin user creation,/api/auth/sessionshape,/api/users. - Rollback anytime:
SOC_IDENTITY_STORE=supabase→ instantly back to the JSON identity path (data intact there; backfill is additive, non-destructive).
- existing account can log in
- new account can register
- session cookie resolves correctly
- page refresh preserves session
- admin can create a user
/api/auth/sessionreturns expected shape/api/usersstill works
npm run backfill:identity
npm run backfill:identity -- --applyAfter apply:
- run the printed
setval(...)SQL in Supabase SQL editor - switch
SOC_IDENTITY_STORE=postgres - run the verification checklist above
- set
SOC_IDENTITY_STORE=supabase - runtime falls back to current Storage JSON identity path
- all auth/session routes work with Postgres enabled
- no user-facing regression
- shadow compatibility confirmed
Move the base profile record into Postgres while keeping avatars in Storage.
- headline
- bio
- location
- website
- specialties
- tools
- avatar metadata
content.portfolio_profilescontent.profile_specialtiescontent.profile_tools
- implement Postgres profile repository
- add feature flag or phased adapter routing
- backfill profile JSON into relational tables
- preserve avatar asset paths from Storage
- test profile read/update flows
/portfolioloads correctly- profile edit saves correctly
- specialties render in same order
- tools render in same order
- avatar still displays correctly
- route reads back to existing JSON profile path
- profile data no longer depends on JSON app-state for primary truth
Move structured portfolio records into relational tables and leave only files in Storage.
- certifications
- certification assets metadata
- education records
content.portfolio_certificationscontent.portfolio_education
- implement certification repository
- implement education repository
- backfill JSON certification/education objects
- preserve file paths for certificate assets
- verify sort order
- create certification
- update certification
- delete certification
- education CRUD still works
- certificate asset links still resolve
- structured credential/education data is relational
- Storage is only asset storage here
Move reports out of Storage JSON and into the operational relational graph.
- report creation
- report listing
- report archive
- report action trail
operations.reportsoperations.report_actions
- implement Postgres report repository
- migrate current
active + archivedreport state - backfill tags and content
- preserve archive history
- remove report JSON as production truth
- Sentinel report list loads
Raporu Okuworks- archive flow works
- active/archive filters work
- report content remains intact
- switch report adapter back to current JSON path
- reports are fully relational
- archive flow remains stable
Move raw/normalized event flow into relational telemetry tables.
- telemetry events
- attack metadata
- source/region/node context
operations.telemetry_events
- define canonical event shape
- map current simulated event payloads to relational form
- implement write path to Postgres
- preserve current UI behavior
- verify timeline ordering and filtering
- live telemetry still renders smoothly
- filters behave the same
- event ordering is preserved
- no repeated-event regression introduced by migration
- telemetry persistence no longer depends on sqlite for truth
Unify telemetry, incidents, and reports into a single relational operational model.
- incident creation
- incident state transitions
- telemetry -> incident linkage
- incident -> report linkage
operations.incidentsoperations.reportsoperations.telemetry_events
- formalize current incident action model
- move incident state transitions to Postgres
- link reports to incidents relationally
- unify analytics and related-event logic
- telemetry actions still work
- incident state transitions remain correct
- report generation from telemetry remains intact
- Sentinel and Home stay visually unchanged
- the operational graph becomes relational and queryable end-to-end
After relational cutovers are proven, remove production dependency on old paths.
- Storage JSON state writes
- sqlite production truth
- dead compatibility glue
- disable JSON identity writes in production path
- disable profile/report JSON primary writes
- reduce sqlite to local/dev-only or remove where appropriate
- remove stale adapter branches
- clean old migration shims
- no product-critical runtime path depends on Storage JSON for truth
- no production-critical path depends on sqlite
Make sure the new backbone is not only correct, but production-strong.
- RLS
- audit
- backup/restore
- monitoring
- data lifecycle
- define RLS strategy per schema
- add audit coverage where needed
- test backup + restore process
- add query/error monitoring
- define retention + archive policies
- restore procedure is documented and tested
- audit trail coverage is acceptable
- data protection posture is production-ready
Every phase must pass these before cutover:
-
buildnpm run build
-
route smoke- affected UI routes open
-
API smoke- affected API routes return expected shape
-
visual smoke- no visible regression in the touched surfaces
-
rollback drill- feature flag or fallback can restore previous runtime behavior
The best next implementation step is:
- apply platform-backbone-v1.sql to Supabase
- write a
users/sessionsbackfill script - run local verification with
SOC_IDENTITY_STORE=postgres - only after that, continue to profile migration
This migration is successful only if:
- users never notice broken flows
- profile/report behavior stays stable during transition
- data ownership becomes simpler after each phase
- repo complexity goes down, not up
- each phase leaves us in a safer place than before