Migration Plan
:::info Source
Sourced from services/identity-service/MIGRATION_PLAN.md in the documentation repo.
:::
1. Overarching Rules
Identity-service participates in the platform-wide backward-compatibility policy (../../docs/roadmap/migration-backward-compatibility.md):
- No breaking changes without a migration window in which both old and new coexist.
- Additive changes (new fields, events, endpoints) are safe by default.
- Breaking changes require a new major version (
v2), dual-publish window ≥ 1 milestone. - JWT claims follow strict CF (Conformist) contract — claim rename requires coordinated rollout with every consumer.
- Offline bundles depend on device public key shape — key format changes require re-registration.
2. Schema Evolution
2.1 Database
| Operation | Allowed Between Minors | Between Majors | How |
|---|---|---|---|
| Add column (nullable) | ✅ | ✅ | Single migration |
| Add column (NOT NULL) | ✅ with default | ✅ | Two-step: add nullable → backfill → alter NOT NULL |
| Drop column | ❌ | ✅ (after deprecation) | Rename to_be_dropped_* → deploy new code → drop in next minor |
| Rename column | ❌ | ✅ | Add new, dual-write, migrate readers, drop old |
| Change column type | ❌ | ✅ | Add new, backfill, switch, drop old |
| Add index | ✅ (CONCURRENTLY) | ✅ | CREATE INDEX CONCURRENTLY |
| Drop index | ✅ | ✅ | DROP INDEX CONCURRENTLY |
| Add table | ✅ | ✅ | — |
| Drop table | ❌ | ✅ | Stop writes → retain read-only ≥ 1 milestone → drop |
All migrations must:
- Apply and roll back on ephemeral Postgres in CI.
- Have a companion
rollback_<name>.sqlor Liquibase<rollback>block. - Be tested against a production-size snapshot in staging before prod deploy.
2.2 RLS Policy Changes
RLS policies are part of the schema contract. Changes require:
- Two-tenant isolation test updated.
- Code review by Security Lead.
- Staged rollout: staging → 10% prod → full prod.
3. API Versioning
3.1 Endpoint Versioning
- URL-major:
/api/v1/…→/api/v2/…(major bump). - Response header
X-API-Version: 1.42reflects minor. Deprecation: true,Sunset: <date>,Link: <doc>; rel="deprecation"on deprecated endpoints.- Dual-serve for ≥ 1 release cycle (typically ≥ 1 milestone).
3.2 Request/Response Evolution
| Change | Versioning |
|---|---|
| Add optional request field | Minor; existing clients unaffected |
| Add required request field | Major; dual-accept window ≥ 1 milestone |
| Add response field | Minor; clients tolerate unknown fields |
| Remove response field | Major; 6-month deprecation in docs + Sunset |
| Narrow enum / type | Major |
| Widen enum / type | Minor — consumers must tolerate unknown enums |
3.3 Error Code Stability
- Error codes in
/openapi/errors.jsonare a frozen contract. - New codes added; existing codes never renamed.
- HTTP status never changed for an existing code.
4. JWT Claim Evolution
JWT claims are a cross-service contract (CF pattern). Changes require platform-wide coordination.
| Claim | Shape | Evolution Policy |
|---|---|---|
sub | UserId (ULID) | Never change format |
tid | TenantId (ULID) | Never change format |
amr | array of strings | Additive (new auth methods) |
scope | space-separated string | Additive (new scopes); deprecation dual-issue |
did | DeviceId | Added in v2; backward-compatible (consumers tolerate absence) |
aud | array | Additive |
iss | string | Never change without platform-wide rotation |
exp, iat, nbf | RFC 7519 | Never change |
kid (header) | opaque | Rotation window ≥ 2 days |
Adding a new claim: ship behind a feature flag; consumers opt-in; flip default after all consumers confirm.
5. Event Versioning
Events follow ../../docs/04-event-driven-architecture.md rules.
| Change | Versioning |
|---|---|
| Add optional payload field | Same v1, minor schema bump |
| Add required payload field | New v2, dual-publish ≥ 1 milestone |
| Remove or rename field | New v2, dual-publish; CI blocks v1 removal until zero consumers |
| New event subject | Safe — add and document |
| Retire event subject | Deprecation event → stop production → zero-consumer → retire |
5.1 Current Identity Events
| Subject | Version | Status |
|---|---|---|
identity.user.registered.v1 | v1 | Active |
identity.user.email_verified.v1 | v1 | Active |
identity.user.logged_in.v1 | v1 | Active |
identity.user.locked.v1 | v1 | Active |
identity.session.revoked.v1 | v1 | Active |
identity.device.bound_for_offline.v1 | v1 | Active |
identity.api_key.issued.v1 | v1 | Active |
identity.api_key.revoked.v1 | v1 | Active |
identity.password.reset_requested.v1 | v1 | Active |
6. Password Hash Migration
- Argon2id parameters may need to be hardened (
m,t) as hardware advances. - Rehash on successful login pattern:
- On login, if stored hash uses older params, rehash with current params and update the row.
- Users who haven't logged in for > 180 days are notified to reset password.
- Old hashes remain verifiable until all users migrate or password reset is forced (tenant policy).
7. MFA Factor Migration
- SMS MFA is deprecated for sensitive scopes (NIST 800-63B). Existing SMS factors kept for compatibility but cannot be added for new high-trust users.
- Users with only SMS prompted to add WebAuthn / TOTP on next login.
- Sunset planned: M5+1 (remove SMS entirely except for legacy regulated flows).
8. SAML / OIDC IdP Configuration Migration
- IdP metadata URL stored per-tenant; refreshed daily.
- Cert rotation from IdP: auto-detected via fresh metadata; old cert remains valid during overlap window.
- Tenant-initiated IdP change: new config validated with test SSO flow before activation; old config stays active until admin flips switch.
9. Offline Bundle / Device Key Migration
- Device public keys are registered at binding.
- If platform migrates from Ed25519 to a new curve:
- New device bindings use new curve.
- Existing bindings remain valid until re-binding.
- Bundles encrypted with key derived from existing public key — backward-compatible.
10. Tenant Migration Runbook
When breaking changes affect multiple tenants:
- Pre-flight: identify affected tenants via analytics (
identity.user.logged_in.v1consumers). - Communication: in-app banner + email 14 days before.
- Opt-in window: tenants activate on schedule.
- Auto-migration: remaining tenants migrated at low-traffic slot.
- Verification: per-tenant smoke test post-migration (login with seeded user in staging replica of prod).
- Rollback plan: documented per change; tested in staging within last 7 days.
- Audit: migration log retained per tenant.
11. Identity backend (provider) migration
Moving between in-house, Keycloak, and vendor OIDC backends:
- Claim stability:
UserIdand JWT claims must remain stable; migration is implemented by re-linkingExternalIdentityrows and preservingusers.id(no user ID churn for in-house users). - Dual-write window: Not required for read-only IdP switch; required if both native passwords and Keycloak must work during cutover — enable dual authentication policies per tenant for a defined window.
- Keycloak import: Bulk export from in-house is optional; preferred path for new enterprise tenants is SSO-only with JIT from Keycloak; existing users get
ExternalIdentitylinks on first successful OIDC login. - Rollback: Revert deployment config to previous
IdentityBackendKind; sessions may be invalidated (documented in runbook).
See SERVICE_OVERVIEW §11–14 for provider taxonomy.
12. Breaking-Change Changelog
Maintained in CHANGELOG.md:
- Every breaking change lists: what broke, migration path, sunset date, consumer coordination status.
- Tagged with semver and GitHub release.