A standalone, NATS-native physical access control (PACS) app that dogfoods the Stone-Age.io platform — RBAC door control with schedules, deny-override, and edge autonomy, composed from the platform's primitives (NATS core, KV, JetStream, PocketBase control plane).
The authorization decision is a small pure function over an in-memory policy
graph (internal/policy), not a rules engine. The central app (accessd) is the
system of record (PocketBase) and mirrors policy to NATS KV one key per record;
edge controllers (access-controller) watch that keyspace and decide locally.
v1 status: the reader is selectable per controller (
controller.reader) — a simulated NATS reader (default; taps arrive over NATS, for dev) or a real OSDP reader on the model's RS485 bus (pure-Go, no cgo, clear-text in v1; Secure Channel is a fast-follow). The lock and door inputs have real drivers alongside the mocks: native GPIO (internal/drivers/gpio, KinCony Server-Mini / CM4) and MCP23017 over I2C (internal/drivers/i2c, KinCony Pi5R8 / CM5). Door monitoring (forced / held-open) and controller heartbeat/health are implemented.
docs/protocol.md— the NATS wire contract: subjects, KV shapes (ACC_POLICY+ACC_STATUS), decision reason codes, audit projection.docs/configuration.md— every config key, default, andSA_env override for both binaries.docs/operators.md— the control-plane access model: operator sign-in, capabilities, collection-rule matrix, and theaudit_logschange log.docs/hardware.md— physical I/O: supported boards, pin maps, relay/input polarity, transports, and how to add a board.
cmd/accessd/ central: PocketBase + KV mirror publisher + audit consumer + controller-health monitor
cmd/access-controller/ edge: policy watcher + pure decision + drivers + door monitoring + heartbeat + optional /status page
internal/policy/ the pure core: Policy types, Decide(), windowOpen()
internal/controller/ PolicyStore (KV watch → maps), tap loop, door state machine, portal/lock arming, commands, heartbeat
internal/drivers/ ReaderDriver / LockDriver / DoorInput / FAIInput interfaces + mocks (MockHardware)
internal/drivers/hardware/ per-model hardware Profile: logical relay/input index → physical line + transport
internal/drivers/gpio/ native GPIO lock + door-input backend (go-gpiocdev, no cgo; Linux only)
internal/drivers/i2c/ MCP23017 lock + door-input backend over I2C (periph.io, no cgo; polled inputs)
internal/drivers/osdp/ OSDP reader: RS485 CP engine (pure-Go, no cgo) + wire codec (osdp/wire); controller.reader: osdp
internal/diag/ opt-in, read-only local /status page of an access-controller's live state (field troubleshooting)
internal/health/ accessd-side heartbeat subscriber → controllers.last_seen/status
internal/authz/ operator capability checks for accessd's custom HTTP routes (commandapi, modelsapi)
internal/commandapi/ UI→NATS command bridge (grant/posture/aux output), gated by the `command` capability
internal/modelsapi/ GET /api/models — enum/options metadata for the UI
internal/changelog/ control-plane audit log: API-driven policy edits + logins → audit_logs collection
internal/audit/ JetStream consumer → PocketBase events collection
internal/natsx/ NATS connection + KV helpers
internal/webui/ the compiled management UI, //go:embed-ed into accessd
pbmigrations/ PocketBase collections (schema-in-code)
ui/ Vue 3 + Vite management UI source (PocketBase-backed CRUD)
accessd serves a Vue 3 management console (locations + a location map, schedules,
portals, controllers, access groups, roles, cardholders, credentials, an events
timeline, a live operational monitor, operator management, and the control-plane
audit log) at /. It is
compiled into internal/webui/public and //go:embed-ed into the accessd
binary — there is no pb_public directory to ship; the binary is
self-contained.
Operators sign in against the built-in users auth collection; their abilities are
an orthogonal set of capabilities (enroll/policy/topology/command/operators)
that gate writes and commands while reads stay open to any authenticated operator —
see docs/operators.md. A PocketBase superuser
(accessd superuser upsert <email> <pass>) is the break-glass account and also
signs into the admin UI at /_.
cd ui && npm install # once
npm run build # → internal/webui/public (commit this)
cd .. && go build ./cmd/accessd
./accessd serve # UI at http://127.0.0.1:8090/ · admin at /_
Always build the UI before the binary; the committed internal/webui/public
means a fresh checkout embeds a working UI without needing npm.
npm --prefix ui run dev # http://localhost:5174, proxies /api + /_ to :8090
Requires Node 20.19+ / 22.12+ (Vite 8).
go test ./...