How it is built
Architecture overview
A physical access control system decides, thousands of times a day, whether a person may open a particular door right now, and keeps a record that cannot be quietly edited. Readers at the door read a credential (a card, a phone, a PIN); a door controller next to the door looks the credential up, checks the rules and drives the lock; a server (often a multi-tenant cloud service) owns the people, the rules and the history. Alarms and video attach to the same events. The idea that makes it work is decisions at the edge, truth at the server: the controller must decide in a fraction of a second and even when the network is down, so it does not ask the server; instead the server compiles "who may go where, when" into a small table, the controller holds a synchronised copy, and every event flows back with a sequence number so nothing is lost or counted twice. The one thing that cannot be decided purely at the edge is state shared between doors (anti-passback), and that is where the design makes an explicit, visible trade-off.
Parts:
- Credentials and readers — what is presented and how it is authenticated —
osdp-reader-protocol(no page yet), APDU smart-card protocol (no page yet), BLE advertising (no page yet), AEAD (AES-GCM, ChaCha20-Poly1305), HKDF key derivation (extract-and-expand) - Door controller — local decision, lock and sensor I/O, offline buffer —
anti-passback-state(no page yet) - Rights compiler — turns people, groups and schedules into the controller's table — Policy engines (PDP/PEP, Cedar, Rego, ReBAC), Row-level security (multi-tenant isolation in the data layer)
- Identity sources — where people and groups come from — SCIM provisioning (no page yet), LDAP directory (no page yet), SAML (no page yet)
- Sync and state — keeping controllers consistent with the server — Incremental sync cursors, Desired-state reconciliation, Device shadow state sync (no page yet)
- Event pipeline and audit — ingest, deduplicate, record — Event deduplication window, Tamper-evident audit log
- Alarms, video and notification — what the guard sees — Alarm management, RTP and RTCP, Video codecs (no page yet), Mobile push delivery
- Device security and updates — identity of each controller, safe firmware — Mutual TLS, X.509 certificates (no page yet), A/B partition updates
- Operations — Process supervision, Graceful draining, Point-in-time recovery, Primary-backup replication, HDR histogram (no page yet), Deterministic simulation testing, Coverage-guided fuzzing
card / phone ──► reader ──OSDP (RS-485, secure channel)──► door controller ──► strike / relay
(APDU, BLE) │ ▲ door contact, REX, tamper
local table (cache) ───┘ │
anti-passback state │ events (seq no., time)
▼
mTLS ◄──────────────────── buffer (flash) ──► ingest ─► dedup ─► audit log
▲ config (desired state) │ │
│ access table (cursor sync) ▼ ▼
identity (SCIM / LDAP / SAML) ─► rights compiler (PDP) ◄── admin UI alarms ─► push ─► guard
▲ └─► video clip (RTP)
cloud DB (row-level security per tenant)
How the parts fit together
Credentials and readers
A reader is deliberately dumb. It reads the credential and passes the data to the controller; it does not decide. That keeps secrets and logic inside the controlled cabinet, because the reader sits on the unsecured side of the door. The link between them is OSDP, an open protocol (IEC 60839-11-5) for a control panel talking to peripheral devices over two-wire RS-485 multi-drop, with a Secure Channel that is encrypted and authenticated (AES-128) (source: LibOSDP README). The older Wiegand wiring, a one-way pulse train with no authentication, is what OSDP replaces. A library such as LibOSDP does the polling: the application calls its refresh function at least every 50 ms to meet the protocol timing (source: LibOSDP README), which is a hard contract for the controller's main loop.
Credentials differ in how much the reader proves:
- Smart card: the reader exchanges APDU smart-card protocol (no page yet) command and response pairs with the card, which holds a key and answers a challenge, so a cloned card number alone does not open the door.
- Mobile credential: Bluetooth Low Energy or NFC between phone and reader. Aliro, a standard from the Connectivity Standards Alliance, defines the communication protocol between access readers and user devices (source: CSA Aliro page), so one wallet credential can work with readers of several makers. In BLE the phone's identity must not be a fixed advertised value or a tracker could follow it; advertising uses rotating identifiers (BLE advertising, no page yet).
- Session keys: a long-term credential key never encrypts traffic directly; HKDF derives a fresh key per transaction from the credential key and a nonce, and the payload is sealed with AEAD so tampering is detected in the same call. A Replay protection window on the reader's counter stops a recorded exchange from being played again.
Why a push notification is not the unlock path: Mobile push delivery is best-effort with throttling, so it carries invitations and alerts ("your key is ready", "door held open"), never the unlock itself, which is local, between phone and reader.
Door controller
The controller holds the table the server compiled: for each credential, its validity window, the groups of doors it opens and the schedules that apply, plus the door's own configuration (lock type, unlock time, held-open limit, holidays). On a read it does a lookup, a schedule check, a state check (anti-passback, door already open) and then drives the relay. The budget from card presented to strike energised is a fraction of a second (order of a few hundred milliseconds), which is why the lookup is local and no network round trip is allowed in the path.
I/O is more than a relay: a door-position contact, a request-to-exit button, a tamper switch and a power fault input. Their changes become events, and a changed contact is debounced before it is trusted. Two safety rules shape the design: the lock fail mode is chosen per door (fail-secure stays locked on power loss, fail-safe releases), and life-safety exit is wired in hardware so no software fault can trap anyone. Software never sits in the egress path.
Anti-passback is a per-credential in-or-out state, anti-passback-state (no page yet): a credential that entered a zone cannot enter again until it has exited, which stops sharing a badge. Within one controller's zone, the state is local and exact. Across controllers (a "global" zone) the state needs a shared view, and there are only two honest options: ask the server in the decision path, and fail open or closed when the link is down, or let controllers exchange state peer-to-peer and accept short windows of disagreement.
Rights compiler
Cardholders belong to roles and groups; doors belong to areas; schedules and holidays qualify when. This is an authorization problem, and the server answers it with a policy engine: evaluate (person, door, time) against rules with default deny and "forbid wins", so a lockdown or a suspended badge overrides any grant, and return which rule decided so the audit can explain it.
The engine's output is not served per request. A compiler evaluates it ahead of time for every person and door and flattens it into the controller's table (credential → door groups and schedules). The server is expressive and slow to change; the controller is simple and fast; the cost is that a rule change must recompile and resync.
In a hosted service many customers share one database. Row-level security puts the tenant predicate inside the database so a forgotten WHERE tenant_id = ? in application code cannot expose another customer's cardholders.
Identity sources
Large customers do not type in cardholders. SCIM provisioning (no page yet) lets an identity provider create, patch and deactivate users through a standard API (SCIM, RFC 7643 and 7644), LDAP directory (no page yet) reads groups from an enterprise directory, and SAML (no page yet) signs administrators in to the management UI with their company login. The rule is that deprovisioning is the fastest path: a deactivated user must leave the table at once, so SCIM deactivation triggers the sync path below instead of waiting for a nightly batch.
Sync and state
Two kinds of data move to controllers, and they use two mechanisms.
- Cardholder data (large, mostly unchanged): Incremental sync cursors. The controller keeps one opaque cursor; each pull returns the changes after it, including tombstones for removals, plus the next cursor. Cost scales with what changed, not with the number of cardholders, which matters when a site has tens of thousands of badges (order of) and a slow uplink. The server pokes the controller on change so a revocation does not wait for a poll interval.
- Configuration (door settings, schedules, firmware version): Desired-state reconciliation. The server stores the desired document; the controller (or an agent for it) reports what it has; a loop diffs the two from full state and applies idempotent fixes, so a missed message only delays convergence. Device shadow state sync (no page yet) is the document shape, with versions so a stale write cannot overwrite a newer one.
A controller that has been offline holds the table it last got. How stale a table may get is a policy: revocations are the dangerous part, and a site that cannot tolerate it uses short credential lifetimes (visitors, contractors) so the controller itself expires them even with no network.
Event pipeline and audit
Every action becomes an event: access granted, denied (with reason), door forced, held open, tamper, power fault, configuration change. A controller stamps each with its identifier, a monotonic sequence number and its clock time, and writes it to a flash buffer before trying to send. On reconnect it replays from the last sequence the server acknowledged, so retransmits are normal and the server uses an Event deduplication window keyed by (controller, sequence) to drop repeats with bounded memory. Sequence gaps reveal lost events.
Events and administrative changes (who granted whom access to which door) go into the Tamper-evident audit log: append-only, with a signed root per batch, so an administrator or intruder cannot erase a door-open record without detection. Controller clocks drift, so the log keeps both the controller's time and the server's receive time.
Alarms, video and notification
A raw event is not an alarm. Alarm management turns events into a few actionable alarms: "door forced" when the contact opens with no valid grant and no request-to-exit, "held open" after a delay; each with a delay or deadband so a bouncing door does not flood the guard, plus acknowledge and shelve states. High-priority alarms reach a guard's phone through push.
Video turns an alarm into evidence. Cameras stream with RTP and RTCP, whose sequence numbers and media-clock timestamps let the recorder cut a clip around the event time; a clip starts at a keyframe, so codec and keyframe interval (Video codecs, no page yet) decide how tight it can be. Camera and controller clocks must agree within a second or two, or the clip misses the door.
Device security and updates
Each controller is a computer on the customer's network with the authority to unlock doors. It authenticates to the server with Mutual TLS using a per-device certificate (X.509 certificates, no page yet): the server knows which controller sent each event, and a revoked certificate cuts a stolen controller off. Firmware updates use A/B partition updates, because a controller at a remote site cannot be reflashed by hand: the new image is tried once, and the old one returns unless the new one reports healthy (reaches the server and answers readers). Doors still work during an update because the other slot's table is untouched.
Operations
The server runs under Process supervision; ingest uses Graceful draining so a deploy hands controllers off without a reconnect storm. Point-in-time recovery covers the cardholder database, and Primary-backup replication keeps it available. HDR histogram (no page yet) tracks command-to-unlock and event-to-alarm latency. The rights compiler is tested under Deterministic simulation testing with simulated controllers, lost links and clocks; Coverage-guided fuzzing targets the reader frames and the sync parser, which take input from outside the trust boundary.
Data and control flow
One badge tap (the path that must not wait):
- The reader reads the credential and sends it over the OSDP secure channel; the controller's refresh loop picks it up within its polling interval.
- The controller looks up the credential in the local table, checks validity, schedule and anti-passback state, and checks the door is not already open.
- On grant it energises the relay for the configured unlock time and starts a held-open timer; on deny it records the reason.
- The event is written to the flash buffer with its sequence number; the anti-passback state changes.
- When the link is up, the event is sent over mTLS; the server deduplicates, writes the audit log and evaluates alarm rules.
One revocation:
- HR deactivates a user; SCIM tells the access server.
- The rights compiler recomputes the person's rows; the change gets a new cursor value with a tombstone.
- The server pokes every controller that holds the person; each pulls from its cursor and removes the row.
- Offline controllers apply it on reconnect. Time spent is the push plus one pull; the exposure window is the controller's offline time.
One alarm:
- A controller sends "door forced" (contact open, no grant).
- Alarm management applies its delay and deadband, raises the alarm and pushes to the guard.
- The recorder cuts a clip around the event time from the camera's stream; the guard acknowledges, and the acknowledge is audited.
Scaling
- Per door the load is tiny; latency and correctness matter. Lever: a bounded, local decision path.
- Per site the limits are the controller's table, flash buffer and uplink: compact rows, cursor sync, a capped event buffer that drops low-priority events first.
- Fleet: many persistent mTLS connections and reconnect storms after an outage. Levers: shard ingest by tenant or controller id, jittered reconnect, draining on deploy, event batching.
- Rule complexity grows compile time; recompile only the affected people and doors.
- Tenants: row-level security first, then dedicated databases for the largest customers.
- Video is the largest byte volume; keep it out of the event path and fetch clips on demand.
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Network down | doors keep working, events delayed, revocations late | controller decides locally from cache | flash buffer with replay, short credential lifetimes for risky groups, alarm on stale sync |
| Duplicate events | doubled entries | retransmit after reconnect | dedup on (controller, sequence) |
| Lost event | gap in the sequence | buffer overflow, flash fault | sequence-gap alert, larger buffer, drop low-priority first |
| Anti-passback lockout | person cannot exit or re-enter | tailgating, badge not read on exit | timed anti-passback, supervised reset, soft mode on global zones |
| Clock drift | schedules open early or late, clip misses | controller clock wrong | time sync, store both controller and receive time, alarm on large skew |
| Door held open false alarms | guards ignore alarms | contact bounce, loading dock use | debounce, delay and deadband, shelving with expiry |
| Replayed mobile unlock | door opens for a recorded exchange | no freshness check | challenge-response with a nonce, replay window |
| Cross-tenant leak | one customer sees another's people | missing tenant filter | row-level security in the database |
| Bad firmware | controller not responding at a remote site | update fault | A/B slots with health confirm, staged rollout |
| Software in the egress path | person trapped in a fire | locks controlled only by software | hardware release on fire alarm and exit hardware, outside the software |
Real implementations
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| LibOSDP | OSDP control-panel and peripheral implementation, with Rust, Python, C++ bindings | goToMain/libosdp | Apache-2.0 | the reader link, AES-128 secure channel |
| uhppoted | building blocks for access control on UHPPOTE TCP/IP controllers: API, CLI, REST, MQTT | uhppoted/uhppoted | MIT | open software around cheap commodity controllers |
| Cedar | policy language and evaluation engine | cedar-policy/cedar | Apache-2.0 | for the rights compiler's policy layer |
| Open Policy Agent | general policy engine (Rego) | open-policy-agent/opa | Apache-2.0 | alternative policy engine |
Commercial platforms (HID, Genetec, LenelS2, Verkada, Gallagher) do not publish their internals.
Sources
- LibOSDP README: github.com/goToMain/libosdp (OSDP, IEC 60839-11-5, Secure Channel AES-128, 50 ms refresh).
- Connectivity Standards Alliance, Aliro.
- P. Hunt et al., SCIM: Core Schema, RFC 7643, and SCIM: Protocol, RFC 7644.
- H. Schulzrinne et al., RTP: A Transport Protocol for Real-Time Applications, RFC 3550.
- H. Krawczyk, P. Eronen, HKDF, RFC 5869; D. McGrew, An Interface and Algorithms for Authenticated Encryption, RFC 5116.
- uhppoted: README.