Team & Access
Smart-lock provider abstraction
All smart-lock operations (Nuki, 2N, future providers) route through one unified LockProvider facade. Operator-visible behaviour is unchanged; the abstraction lets new lock vendors plug in cleanly and makes error handling consistent.

What the operator sees (unchanged)
- Smart Locks screen shows one card per property with a lock configured.
- Codes editor + audit log + activity log per property.
- Same buttons, same behaviour — Generate PIN, Revoke, Test Connection.
What changed underneath
- One API — every lock call (generate code, revoke code, unlock, get activity) goes through
lockOps.<operation>with the property's provider auto-resolved. - Retry policy consistent — 5 attempts on transient failures across ALL lock calls.
- Never-silent-error — every lock error surfaces on the operator screen + writes an
app_errorsrow; a transient Nuki 5xx is retried, never silently dropped. - Adds new providers cleanly — adding a new lock vendor requires implementing the
LockProviderinterface + registering in the provider registry. Four steps, documented indocs/lock-integration-api.md.
Why this matters for the operator
- Error visibility — when a lock call fails, you see it: the failure surfaces on screen and a retry runs.
- Consistent behaviour across providers — same retry, same error UX regardless of Nuki vs 2N vs a future provider.
- New provider path — if a tenant wants to onboard a new lock brand (Salto, August, TTLock, …), the platform can add it without rewriting every callsite.
What's still per-provider
- Provider capabilities — each provider declares what it supports (
generateCode,revokeCode,unlock,getActivity). Not every provider supports every capability. The V28 Smart Locks screen shows disabled controls with a tooltip when the property's provider doesn't support the operation (mirrors the Channel sync capability-matrix pattern). - Provider-specific config — Nuki API token vs 2N intercom IP address vs whatever the new provider needs. Configured per-property in Settings.
2N lifecycle: dispose
2N sessions require an explicit dispose call to release resources on the intercom side. The abstraction handles this automatically — no operator step needed.
No-direct-import ratchet
A CI ratchet (server/tests/ratchets/noDirectLockClientImportRatchet.test.ts) fails PR if any file imports Nuki or 2N clients directly instead of going through lockOps. Prevents drift back to the prior implementations.
Source: the luxury.host operator manual.