-- Migration 002: who is allowed to ask, and what a fresh PC is told. -- -- 001 built the data. Nothing could read it: the only writer was the MQTT -- consumer, authenticated by the broker. This adds the request/response half — -- staff logging in from the desktop app, and a newly installed store PC -- collecting its own broker credentials. BEGIN; -- ============================================================ sessions ===== -- Opaque tokens in a table, not JWTs. -- -- A JWT cannot be revoked without a blocklist, which is a session table with -- extra steps and worse failure modes. This system holds biometric data on -- shop-floor PCs that get lost, resold and shared between staff, so "log that -- device out, now" has to actually work. At this volume the lookup is one -- indexed read. -- -- Only the SHA-256 of each token is stored. A database dump then contains no -- usable session — and SHA-256 rather than bcrypt because the token is 256 -- bits from crypto/rand, so there is no dictionary to slow an attacker down -- through, only a per-request cost to pay. CREATE TABLE sessions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES app_users(id) ON DELETE CASCADE, -- Denormalised from app_users so the auth lookup is one row, and so a -- session cannot outlive a user being moved between clients. client_id uuid REFERENCES clients(id) ON DELETE CASCADE, access_hash bytea NOT NULL UNIQUE, refresh_hash bytea NOT NULL UNIQUE, access_expires_at timestamptz NOT NULL, refresh_expires_at timestamptz NOT NULL, revoked_at timestamptz, -- Enough to tell one device from another in a session list. Not an -- identifier, and deliberately not an IP address: a shop's IP tells us -- where a customer's staff live, which we have no reason to keep. device text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), last_used_at timestamptz ); CREATE INDEX sessions_user_idx ON sessions (user_id) WHERE revoked_at IS NULL; CREATE INDEX sessions_expiry_idx ON sessions (refresh_expires_at); -- ============================================================ enrolment ==== -- A one-shot token that turns an anonymous install into a known site. -- -- The installer ships with no credentials at all, so a leaked build hands out -- nothing. The operator types this code once; the server answers with the -- broker credentials for exactly one site and marks the token spent. CREATE TABLE site_enrolment_tokens ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE, site_id uuid NOT NULL REFERENCES sites(id) ON DELETE CASCADE, token_hash bytea NOT NULL UNIQUE, label text NOT NULL DEFAULT '', expires_at timestamptz NOT NULL, used_at timestamptz, created_by uuid, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX site_enrolment_site_idx ON site_enrolment_tokens (site_id); -- The broker password this site publishes with, encrypted with the server's -- own key (AES-GCM, BEHAVISION_SECRET_KEY). -- -- It has to be recoverable, not hashed: enrolment HANDS IT OUT. Encrypting it -- means a stolen database dump is not a set of live broker logins, which a -- plaintext column would be. Mosquitto keeps its own hashed copy in its passwd -- file; this is the second half of a pair that must be provisioned together. ALTER TABLE agents ADD COLUMN mqtt_password_enc bytea; -- ============================================================ health ======= -- Reported by the heartbeat, kept on the agent row because they describe the -- site right now, not a history worth querying. -- -- fraction_below_gate is the one that matters: the share of faces this site's -- cameras saw that fell under the enrolment gate. A footfall number from a -- badly placed camera is wrong in a way nobody can see from the number itself, -- so the figure has to travel with its own confidence. Measured on the Office1 -- camera it was 0.727 — 73% of visitors seen and discarded, while the report -- said what looked like a quiet week. ALTER TABLE agents ADD COLUMN fraction_below_gate real; ALTER TABLE agents ADD COLUMN cameras_total integer NOT NULL DEFAULT 0; ALTER TABLE agents ADD COLUMN cameras_up integer NOT NULL DEFAULT 0; ALTER TABLE agents ADD COLUMN spool_queued integer NOT NULL DEFAULT 0; -- Events a site lost because it was offline long enough to overflow its own -- queue. Cumulative and never reset: a footfall figure with known-lost events -- behind it must say so. ALTER TABLE agents ADD COLUMN spool_dropped bigint NOT NULL DEFAULT 0; COMMIT;