Skip to main content

EVENT_SCHEMAS — lock-integration-service

Bundle: SERVICE_OVERVIEW · DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS

Cross-cutting: docs/04 — Event-Driven Architecture. Naming: docs/standards/NAMING — Events. All payloads carry the canonical envelope from 04 §4 Envelope.

Pub/Sub topic name = event subject (1:1). All subjects under melmastoon.lock.*.

1. Envelope (recap)

Every event uses the platform envelope:

{
"specVersion": "1.0",
"id": "evt_01HX...",
"subject": "melmastoon.lock.credential.issued.v1",
"type": "melmastoon.lock.credential.issued.v1",
"source": "/services/lock-integration-service",
"time": "2026-04-30T10:13:42.117Z",
"tenantId": "tnt_01HX...",
"traceId": "00-...-01",
"correlationId": "rsv_01HX...",
"causationId": "evt_01HW... (the reservation.confirmed.v1 that started this saga)",
"schemaUrl": "https://schemas.melmastoon.ghasi.io/lock/credential.issued.v1.json",
"data": { /* per-subject payload below */ }
}

2. Produced events

2.1 melmastoon.lock.credential.requested.v1

Emitted when the saga starts an issuance. Retention class: operational (90d). Consumers: audit-service, analytics-service.

{
"keyCredentialId": "key_01HX...",
"propertyId": "ppt_01HX...",
"reservationId": "rsv_01HX...",
"guestId": "gst_01HX...",
"rooms": ["rmu_01HX..."],
"validFrom": "2026-05-01T14:00:00Z",
"validUntil": "2026-05-03T11:00:00Z",
"preferredKinds": ["mobile_app", "pin_code"],
"vendor": "ttlock",
"idempotencyKey": "sha256:9c1f..."
}

2.2 melmastoon.lock.credential.issued.v1

Vendor confirmed issuance; credential state = active. Retention class: regulated (7y, Merkle-anchored). Consumers: notification-service (mobile-key/PIN delivery), audit-service, analytics-service, sync-service (desktop projection).

{
"keyCredentialId": "key_01HX...",
"propertyId": "ppt_01HX...",
"holderKind": "guest",
"reservationId": "rsv_01HX...",
"guestId": "gst_01HX...",
"kind": "mobile_app",
"rooms": ["rmu_01HX..."],
"scope": { "areas": ["lobby", "gym"] },
"validFrom": "2026-05-01T14:00:00Z",
"validUntil": "2026-05-03T11:00:00Z",
"vendor": "ttlock",
"provisional": false,
"delivery": {
"artifact": { "type": "mobile_token", "opaqueRef": "tk_01HX..." },
"deepLink": "melmastoon://tenant/silk/keys/key_01HX..."
},
"issuedAt": "2026-04-30T10:13:42Z",
"warnings": []
}

vendorRef is never in this payload. The delivery.artifact.opaqueRef is single-use, exchanged at bff-tenant-booking-service for the actual mobile-key bytes.

2.3 melmastoon.lock.credential.failed.v1

{
"keyCredentialId": "key_01HX...",
"reservationId": "rsv_01HX...",
"vendor": "ttlock",
"failureReason": "vendor_unreachable",
"vendorMessage": "<scrubbed; no PII or credential>",
"attempts": 5,
"lastAttemptAt": "2026-04-30T10:14:11Z"
}

Retention: regulated (7y).

2.4 melmastoon.lock.credential.updated.v1

{
"keyCredentialId": "key_01HX...",
"changes": {
"validUntil": { "from": "2026-05-03T11:00:00Z", "to": "2026-05-04T11:00:00Z" },
"rooms": { "from": ["rmu_01HX..."], "to": ["rmu_01HY..."] }
},
"vendor": "ttlock",
"updatedAt": "2026-05-02T08:32:01Z"
}

2.5 melmastoon.lock.credential.revoked.v1

{
"keyCredentialId": "key_01HX...",
"reservationId": "rsv_01HX...",
"vendor": "ttlock",
"reason": "checkout",
"revokedAt": "2026-05-03T11:00:01Z",
"metadata": { "wasProvisional": false, "vendorRevokeOk": true }
}

2.6 melmastoon.lock.credential.suspended.v1 / .unsuspended.v1

{
"keyCredentialId": "key_01HX...",
"reason": "no_show",
"suspendedAt": "2026-05-01T16:00:00Z"
}

unsuspended.v1 carries unsuspendedAt and the originating unsuspendedBy: 'usr_...|saga'.

2.7 melmastoon.lock.device.registered.v1

{
"deviceId": "lck_01HX...",
"propertyId": "ppt_01HX...",
"vendor": "salto",
"label": "Room 204 main door",
"rooms": ["rmu_01HX..."],
"capabilities": { /* full LockCapabilities */ },
"firmware": "v3.2.1",
"registeredAt": "2026-04-15T09:00:00Z"
}

2.8 melmastoon.lock.device.health_alert.v1

Emitted when healthCheck crosses a threshold (battery low, clock skew, offline, firmware out-of-date).

{
"deviceId": "lck_01HX...",
"propertyId": "ppt_01HX...",
"alert": "clock_skew",
"severity": "warning",
"metric": { "clockSkewMs": 612000 },
"threshold": { "warnAtMs": 300000, "alertAtMs": 600000 },
"observedAt": "2026-04-30T08:55:00Z"
}

2.9 melmastoon.lock.device.battery_low.v1

{
"deviceId": "lck_01HX...",
"propertyId": "ppt_01HX...",
"batteryPct": 18,
"thresholdPct": 25,
"observedAt": "2026-04-30T07:32:00Z"
}

2.10 melmastoon.lock.device.offline.v1 / .online.v1

{
"deviceId": "lck_01HX...",
"propertyId": "ppt_01HX...",
"since": "2026-04-30T08:55:00Z",
"lastSeenAt": "2026-04-30T08:54:42Z",
"reason": "no_heartbeat"
}

2.11 melmastoon.lock.master_key.issued.v1

{
"masterKeyId": "mky_01HX...",
"keyCredentialId": "key_01HX...",
"staffUserId": "usr_01HX...",
"propertyId": "ppt_01HX...",
"scope": { "kind": "floor", "floor": "3" },
"validFrom": "2026-05-01T06:00:00Z",
"validUntil": "2026-05-01T18:00:00Z",
"kind": "rfid_card",
"issuedAt": "2026-05-01T05:55:12Z"
}

Retention: regulated (7y).

2.12 melmastoon.lock.master_key.expired.v1

{
"masterKeyId": "mky_01HX...",
"keyCredentialId": "key_01HX...",
"staffUserId": "usr_01HX...",
"expiredAt": "2026-05-01T18:00:01Z",
"trigger": "shift.ended"
}

2.13 melmastoon.lock.encoder_session.opened.v1 / .closed.v1

// opened
{
"encoderSessionId": "enc_01HX...",
"propertyId": "ppt_01HX...",
"desktopDeviceId": "dev_01HX...",
"encoderModel": "hid_omnikey",
"transport": "usb_hid",
"openedAt": "2026-04-30T05:00:00Z"
}

// closed
{
"encoderSessionId": "enc_01HX...",
"closedAt": "2026-04-30T15:30:00Z",
"closeReason": "graceful"
}

Retention: operational (90d).

2.14 melmastoon.lock.audit.attempt.v1

Door-access event ingested from a vendor webhook. Retention: regulated (7y, Merkle-anchored).

{
"attemptId": "kca_01HX...",
"propertyId": "ppt_01HX...",
"deviceId": "lck_01HX...",
"keyCredentialId": "key_01HX...",
"outcome": "granted",
"denyReason": null,
"vendor": "ttlock",
"vendorEventId": "<opaque>",
"attemptedAt": "2026-05-01T14:32:11Z",
"ingestedAt": "2026-05-01T14:32:13Z"
}

2.15 melmastoon.lock.vendor_webhook.received.v1

Pre-dispatch audit. Retention: operational (90d).

{
"webhookId": "whk_01HX...",
"vendor": "ttlock",
"externalEventId": "<opaque>",
"type": "access.granted",
"receivedAt": "2026-05-01T14:32:13Z",
"signatureOk": true
}

2.16 melmastoon.lock.vendor_webhook.processed.v1

{
"webhookId": "whk_01HX...",
"vendor": "ttlock",
"type": "access.granted",
"outcome": "applied" | "duplicate" | "ignored",
"processedAt": "2026-05-01T14:32:13Z"
}

2.17 melmastoon.lock.vendor_adapter.health_changed.v1

Circuit-breaker state change. Retention: operational (90d).

{
"vendorAdapterId": "vad_01HX...",
"vendor": "salto",
"environment": "production",
"from": "closed",
"to": "open",
"reason": "error_rate_threshold",
"metric": { "errorRatePct": 38.0, "windowSize": 100 },
"changedAt": "2026-04-30T11:14:22Z"
}

3. Consumed events

SubjectProducerSaga stepIdempotency key
melmastoon.reservation.booking.confirmed.v1reservation-serviceissue saga (§3.1 in APPLICATION_LOGIC)sha256(reservationId+'issue'+'v1')
melmastoon.reservation.booking.cancelled.v1reservation-servicerevoke sagasha256(eventId+keyCredentialId+'revoke')
melmastoon.reservation.booking.checked_out.v1reservation-servicerevoke sagasame shape
melmastoon.reservation.booking.early_checkout.v1reservation-servicerevoke saga (immediate)same shape
melmastoon.reservation.booking.dates_changed.v1reservation-serviceupdate sagasha256(reservationId+'update'+version)
melmastoon.reservation.booking.no_show.v1reservation-servicesuspend saga (delayed by policy.noShowSuspendAfterHours)sha256(reservationId+'suspend-noshow')
melmastoon.reservation.booking.fraud_flagged.v1reservation-servicesuspend sagasha256(reservationId+'suspend-fraud')
melmastoon.staff.shift.started.v1staff-service / iam-serviceissue mastersha256(shiftId+'issue-master')
melmastoon.staff.shift.ended.v1staff-service / iam-servicerevoke mastersha256(shiftId+'revoke-master')
melmastoon.iam.user.deactivated.v1iam-servicebulk-revoke active masters held by usersha256(userId+'mass-revoke-iam')
melmastoon.tenant.property.deactivated.v1tenant-servicebulk-revoke active credentials at propertysha256(propertyId+'mass-revoke-property')
melmastoon.tenant.config_updated.v1tenant-servicereload KeyKindPolicy cache (no key transitions)n/a

4. Retention classes

ClassPub/Sub message retentionBigQuery sink retentionPostgres operational store
operational7d90d90d (rolled to BQ)
regulated7d7yindefinite (until tenant deletion + retention window)
audit7d7y + Merkle anchorindefinite

The lock.audit.attempt.v1 and all credential.* events except requested are regulated (with the lifecycle ones additionally Merkle-anchored via audit-service).

5. Schema registry

Every payload above has a JSON Schema published at https://schemas.melmastoon.ghasi.io/lock/<subject>.json. The event_schema_registry CI gate fails the build on any payload that does not validate. New .v<n+1> topics ship with backwards-compatible additions (additive optional fields); breaking changes ship a new topic and dual-publish for one full release window per 04 §6.

6. Cross-tenant guard

Every produced event carries tenantId in the envelope. The outbox publisher rejects any payload whose tenantId differs from the aggregate root's tenantId (CI test in outbox.spec.ts). Subscribers re-validate on inbound and refuse cross-tenant correlation.

7. Local desktop events (offline path)

When the Electron desktop main process issues a credential offline, it writes a local-outbox row with subject melmastoon.lock.credential.issued.local.v1 (note .local). This subject is never published to Pub/Sub directly; it is drained on /sync/v1/push by sync-service and the cloud reconciler emits the canonical melmastoon.lock.credential.issued.v1 subject after materialization (or revoked.v1 if the reservation has changed).

// melmastoon.lock.credential.issued.local.v1 (desktop-local, never on Pub/Sub)
{
"localId": "prov_keycred_01HX...",
"tenantId": "tnt_01HX...",
"propertyId": "ppt_01HX...",
"reservationId": "rsv_01HX...",
"kind": "rfid_card",
"rooms": ["rmu_01HX..."],
"validFrom": "...",
"validUntil": "...",
"vendorRef": "<encoder-card-serial>",
"certSerial": "lock-oic-2026-04-30-01HX...",
"issuedAt": "...",
"deviceSig": "<ed25519 sig of payload by desktop privkey>"
}

The same desktop emits melmastoon.lock.credential.revoked.local.v1 on offline checkout/cancellation; reconciliation drains it the same way.