Skip to main content

MIGRATION_PLAN — lock-integration-service

Bundle: SERVICE_OVERVIEW · DOMAIN_MODEL · APPLICATION_LOGIC · DATA_MODEL · SERVICE_READINESS · SERVICE_RISK_REGISTER

Cross-cutting: docs/architecture/ADR-0004, docs/09 §10 Migration paths.

This service is greenfield (no predecessor monolith), but it must support two recurring kinds of migration from day one: vendor onboarding (a new property choosing a vendor we already support) and legacy lock migration (replacing an incumbent system at an existing property). This document also covers internal schema/event evolution.

1. Internal evolution (schema + events)

1.1 Database migrations

  • Driven by Drizzle ORM migrations under services/lock-integration-service/src/infra/db/migrations/.
  • Forward-only; rollback by writing a new forward migration that reverses the change.
  • Per SERVICE_TEMPLATE and 50-data, follow the expand → migrate → contract pattern:
    1. Expand: add new columns/tables nullable; deploy.
    2. Backfill: dual-write or batch backfill via Cloud Run Job; verify.
    3. Switch reads: deploy code reading new columns.
    4. Contract: drop old columns / deprecate after one full release cycle.
  • All migrations run via db-migrate Cloud Run Job before the application traffic shift in DEPLOYMENT_TOPOLOGY §8.
  • RLS policies must be re-asserted after every column addition that affects access control.

1.2 Event schema versioning

  • Subjects use melmastoon.lock.<aggregate>.<verb>.v<n> per NAMING.
  • Adding optional fields → same v<n> (consumers tolerate unknown ignored fields).
  • Removing/renaming/breaking semantics → new v<n+1> published in parallel; producers dual-publish for one full release window; consumers migrate; old version retired.
  • The schema registry enforces compatibility checks on every PR (04 §schema registry).

1.3 API versioning

  • REST is /api/v1. Breaking changes go to /api/v2; both run in parallel for ≥ one release.
  • OpenAPI spec is the source of truth; client SDKs regenerated on every release.

2. Vendor onboarding (new vendor adapter)

When the platform adds support for a new lock vendor (e.g., Onity, Kaba):

  1. Spike — research vendor SDK, capability matrix, latency/cost. Output: ADR-0004 amendment.
  2. Adapter — implement LockAdapter interface (DOMAIN_MODEL §4) with full per-method coverage; declare capabilities().
  3. Simulator — implement matching simulator in packages/lock-vendor-simulators/ so all higher-tier tests can run without sandbox.
  4. Contract suite — adapter contract tests pass on simulator; opt-in green run against vendor sandbox.
  5. Webhook ingress — register handler under /webhooks/v1/{vendor}; signature scheme implemented.
  6. Secrets — add vendor-cred shape to Secret Manager schema; rotation cadence published.
  7. Observability — add per-vendor labels to existing metrics (no new metrics needed); add nightly synthetic check.
  8. Docs — update DOMAIN_MODEL §4, SECURITY_MODEL §5.1, SERVICE_RISK_REGISTER.
  9. Pilot — enable for a single property as provisional (manual ops oversight) for 30 days. Promote to GA per SERVICE_READINESS §1.

No data migration required — new vendor onboarding is additive.

3. Vendor switch at an existing property (legacy → new vendor, or vendor → vendor)

This is the harder case: a property is replacing its physical lock hardware (or commercial decision to switch vendors). The cutover must avoid a window where guests can't open their door.

3.1 Phases

PhaseDurationAction
Plan1–2 weeksInventory of rooms × vendors; reservation horizon analysis; staff-shift overlap schedule; legacy-vendor revocation plan
Dual-vendor enable1 dayAdd new VendorAdapter row for the new vendor (status active); update KeyKindPolicy so preferredOrder lists new vendor first, old vendor second as fallback
Hardware cutover (per room)rollingField engineer installs new lock on the room; LockDevice.status for old marked decommissioned; new device registered as active
Dual-issue windowoverlap with hardware cutoverFor any reservation whose stay overlaps a still-not-cutover room, new credentials are issued on the legacy vendor; once room is cutover, new credentials issue on the new vendor; live credentials on the legacy vendor remain valid until checkout
Drainuntil last legacy credential's validUntilNo new issuance against legacy vendor; webhooks still ingested for safety
Decommission1 day after drainVendorAdapter for old vendor → disabled; vendor_credentials rotated and then destroyed; documentation update

3.2 Tooling

pnpm lock:cli vendor-switch script automates phases 2–6:

pnpm lock:cli vendor-switch \
--property ppt_01... \
--from salto --to ttlock \
--plan ./switch-plan.json # generated

Plan file lists per-room cutover dates and a sanity check on overlapping reservations.

3.3 Saga behavior during dual-vendor

The IssueSaga consults LockDevice.vendor (after the room cutover) rather than KeyKindPolicy.vendor directly to avoid mis-issuing on the wrong vendor mid-cutover. The use case IssueKeyCredentialUseCase selects vendor as:

vendor = property.lockDevice(roomId).vendor // physical truth wins

3.4 Risk

R-LOCK-23 in SERVICE_RISK_REGISTER. Pilot-tested on one room before any rolling cutover.

4. Legacy lock migration (no prior platform — first vendor adoption)

A property joining the platform with existing physical locks but no platform integration yet.

4.1 Two cases

  • Vendor cloud already in use (e.g., Salto KS already operational at the property): import existing locks via POST /api/v1/lock-devices/bulk-import?vendor=salto&adapterId=... which calls vendor listDevices() and registers each as a LockDevice. Existing vendor credentials at the vendor are not ingested — guest credentials going forward are managed by the platform; legacy credentials remain managed by the vendor portal until they expire naturally.
  • No vendor cloud / standalone locks (Generic Wiegand): pair the Electron desktop with the encoder; for each room, run the desktop "register lock" flow which creates a LockDevice row with vendor='generic-wiegand' and a synthetic vendor_device_ref.

4.2 Backfill of existing reservations

If the property has live reservations imported from a PMS migration, the platform's reservation-service migration emits reservation.imported.v1 events. The lock-integration-service does not auto-issue keys for imported reservations whose check-in is in the past — operators may explicitly call POST /api/v1/key-credentials per imported reservation if needed.

5. Data migration (intra-platform reshape)

If a future schema change requires data reshape (e.g., splitting key_credentials.scope.areas[] into a child table):

  1. Add new table; deploy.
  2. Backfill via Cloud Run Job batched by tenant; emit lock.key_credential.scope_migrated.v1 (operational event, not regulated).
  3. Switch reads on next deploy.
  4. Contract column after one release.

Backfill jobs are idempotent and resumable from a checkpoint table migration_checkpoints(name, last_id, updated_at).

6. Rollback strategy

  • Code: Cloud Run keeps last 10 revisions; gcloud run services update-traffic --to-revisions <prev>=100 rolls back.
  • DB: forward-only; rollback = forward migration. Never DROP COLUMN immediately.
  • Events: dual-publish window means consumers can downgrade by stopping consumption of v<n+1>; producers continue publishing both.
  • Vendor adapter: vendor_adapters.status='disabled' halts an adapter at runtime without a deploy.

7. Cross-references