Skip to content

System Architecture

Overview

OpenLatch is an electronic door lock system for a hackerspace. It controls physical entry via NFC cards, making access decisions locally on an ESP32 while keeping membership and rules management on a central backend server.

The system is designed offline-first: the ESP32 can make most access decisions without a network connection, using a cryptographically signed allow-list cached in local flash storage.

Components

ESP32 Controller (Firmware)

The ESP32 is the brain at the door. It:

  • Reads NFC card UIDs from the CONLAN M1200 terminal via Wiegand protocol
  • Looks up the UID in a locally cached allow-list
  • Evaluates access conditions (time slots, date ranges, grace periods)
  • Controls the NUKI Smart Lock Ultra via Bluetooth LE
  • Provides feedback via LEDs and buzzer
  • Periodically connects to the network to sync the allow-list from the backend
  • Handles the physical shutdown button inside the space (for space shutdown — see terms.md)

Networking: Wired Ethernet is recommended for production (main/exterior doors) for reliability and to avoid WiFi/BLE radio contention. WiFi is fine for development and non-critical interior doors. When using WiFi, the ESP32's single radio is shared between WiFi and BLE via time-division: WiFi on during sync (every 5 minutes), BLE available the rest of the time for NUKI control. With Ethernet, BLE is always available.

CONLAN M1200 NFC Terminal

An outdoor IP67-rated NFC reader mounted next to the door. It:

  • Handles MIFARE DESFire EV2/EV3 challenge-response authentication internally
  • Outputs the authenticated card UID over Wiegand (26 or 34 bit)
  • The ESP32 only needs to decode the Wiegand signal — no NFC crypto on the ESP32

NUKI Smart Lock Ultra

A battery-powered smart lock on the door. The ESP32 controls it via BLE using the NukiBleEsp32 library. Supports lock, unlock, and unlatch (full bolt retraction) commands.

Backend Server

A Python FastAPI application managing:

  • Members: Registration, roles (admin/keyholder/member/guest), activation
  • NFC Keys: Registration, assignment to members, revocation, expiry
  • Access Rules: Time-based, date-based, role-based, guest sponsorship, emergency, conditional
  • Allow-List Generation: Evaluates all rules, builds a binary payload, signs it with Ed25519
  • Audit Log: Records all access-relevant events
  • Admin UI: Web interface for managing the above (planned)

PostgreSQL Database

Stores members, NFC keys, access rules, audit log, and allow-list version history.

Data Flow

Normal Access (Offline)

1. Person presents NFC card to CONLAN M1200
2. Terminal authenticates card (DESFire) and sends UID via Wiegand
3. ESP32 decodes Wiegand frame, extracts UID
4. ESP32 searches UID in RAM-cached allow-list
5. ESP32 evaluates access conditions (type, time slots, date range, grace period)
6. If granted: ESP32 sends BLE unlatch command to NUKI
7. LED green + beep. Door unlocks for 5 seconds, then auto-relocks.

No network connectivity required for steps 1-7.

Allow-List Sync (Periodic)

1. Every 5 minutes, ESP32 disconnects BLE and connects to the network
2. ESP32 sends GET request with current allow-list version
3. Backend checks if a newer version exists
4. If yes: backend evaluates all rules, builds binary payload, signs with Ed25519
5. ESP32 receives payload, verifies signature, checks version monotonicity
6. ESP32 writes new allow-list to LittleFS flash storage
7. ESP32 reloads allow-list into RAM
8. ESP32 reports any queued events (access logs, anomalies, emergency alerts)
9. ESP32 disconnects from the network, re-enables BLE

Conditional Access (Online)

For backup keyholders who need organizer approval:

1. Card scanned, ESP32 finds ACCESS_CONDITIONAL entry
2. ESP32 connects to the network, sends approval request to backend
3. Backend notifies the associated organizer (push notification)
4. ESP32 waits up to 60 seconds for response
5. If organizer approves: door opens. Otherwise: denied.

Binary Protocol (OL02)

The allow-list uses a compact binary format optimized for embedded parsing:

  • Header (32 bytes): magic, version, timestamps, entry count, flags
  • Entries (48 bytes each): UID, access type, date range, grace period, notification flags, up to 4 time slots
  • Signature (64 bytes): Ed25519 over header + entries

For 200 members: ~9.7 KB total. Fits easily in a single HTTPS response and in ESP32 flash.

See proto/allowlist.md for the full specification.

Security Layers

Layer Protection
NFC Card DESFire EV2/EV3 challenge-response (anti-cloning)
Wiegand Physical wires inside the wall (not exposed)
Allow-List Ed25519 signature (anti-tampering)
Transport HTTPS (encryption in transit)
Replay Monotonic version numbers + validity window
Credentials NVS encryption on ESP32
Passwords Argon2id hashing
Admin API JWT with short-lived tokens
Audit All events logged with timestamps

See docs/threat-model.md for detailed threat analysis.

Technology Choices

Component Choice Why
Backend language Python Accessible for hackerspace contributors
Backend framework FastAPI Async, auto-docs, Pydantic validation
Database PostgreSQL Relational data, JSONB for rule parameters
ORM SQLAlchemy 2.0 (async) Mature, well-documented
Migrations Alembic Standard for SQLAlchemy
Firmware framework Arduino via PlatformIO NukiBleEsp32 library support, large ecosystem
Firmware tests Unity (PlatformIO) Native host tests without hardware
Signing Ed25519 (PyNaCl / libsodium) Modern, fast, small keys, no patents
CI/CD GitLab CI TDD pipeline with Docker-based builds

See docs/adr/ for detailed Architecture Decision Records.