TechDash

Security

The gates, and where they are.

Written for the person who will be asked to sign off on this. Each item below describes a mechanism rather than a posture, so it can be checked.

Signing in

Google Workspace, restricted to your domain, failing closed

Interactive sign-in is Google Workspace SSO restricted to the district's configured domain. The comparison is an exact equality test, so a subdomain, a trailing dot, a look-alike suffix and a second @ are all refused. The gate runs before any account record is created, so a refused address leaves nothing behind. If the domain setting is blank the server admits nobody, logs the misconfiguration and says why.

The app never receives a session in a URL

The app receives a single-use code bound to a PKCE challenge and trades it for a session over a POST. The code expires in two minutes and is worthless without the verifier, which never leaves the phone.

Sign-in can only return to an address on an exact allowlist

The return address is compared as a complete string — no prefix match, no scheme-only check, no case folding — against a list the district's own operator controls. It is checked a second time at the moment the redirect is issued, so a value that changed after the first check cannot be used. Rejected attempts are logged with the scheme and host only, never the full address the caller supplied.

One honest footnote. An older sign-in style that did carry a session in the URL still exists for a single retired, district-specific app that is not distributed. It is confined to that app's own URL scheme and governed by a configuration switch that is deliberately still on, so the retired app keeps working until it is off every phone. It will be switched off, and this page will say so when it is.

Files and records

Uploaded files inherit the permission of the record they belong to

The upload directory is denied wholesale at the web server, and every gated file is served by a route that re-applies the owning record's own rule. A ticket attachment is checked against the ticket, and a file on an internal note stays hidden from the requester. A knowledge-base attachment is checked against the article's visibility including its category ancestry. A vault attachment is checked against the parent configuration's contains-secrets flag.

An unguessable URL is never treated as protection, and a new module's upload directory inherits the deny and fails closed until somebody gives it a route.

Looking at usage data is itself an audited act

The usage dashboard requires an administrator and an identity named explicitly in configuration. An empty list means nobody, including administrators, and there is no "all admins" setting. Every open, every per-person drilldown, every export and every denial is written to an audit table that the module itself displays, with the viewer's identity and the person they looked at.

Hardware actions

A driver that cannot do something safely refuses, with a reason

Door hardware is driven through swappable drivers, and each declares exactly which features its hardware supports. An action a driver cannot perform safely is refused with an explanation rather than failing silently — one shipped panel type supports only unlocked, card-only and schedule modes and declines a lock command by design, because a lockdown that silently fails is worse than one the interface declines.

Every command sent to a device is written to a queryable, exportable action log, and sensitive actions such as revealing a PIN or arming a lockdown are recorded in a separate audit table that is never pruned.

A read-only audit that is read-only by construction

The paging coverage audit talks to handsets and ceiling speakers through a client that permits an explicit list of method-and-path pairs and refuses everything else, so a configuration write cannot be issued even by accident. No device credential is ever written into the audit record.

Privacy

What the app stores, what leaves the device, what is retained and for how long, and how to exercise a data right, are all in the privacy policy.

Read the privacy policy →