A page that says “built” is asking to be believed. Each card below carries the file or constant a reader with the repository can check the sentence against, and the four status words are kept distinct on purpose: built means the engine is persisted and pinned by a test; substrate means the store exists and the surface reading it does not; dark means the decision exists and always resolves to a refusal today, by construction; unprovisioned means no engine is connected and the seam answers with a service-unavailable response rather than fabricating one. Collapsing those into a single word would hide the difference between a transport we have not connected and a family who said no.
The student-level consent gate — one chokepoint, fail-closed
Before a notification has a recipient at all, the student it is about is resolved through a single canonical suppression chokepoint. A student who is publication-suppressed, whose family denied the disclosure basis, or whose consent context cannot be resolved at that moment produces ZERO recipients — not a warning, not a queued message held for review, not a fallback to a cached decision. The basis is directory information, deliberately the conservative choice: an operator may argue that an absence notification is reachable without it, and widening that is a decision for counsel and the district, not a code default. There is no second copy of the consent rule inside the notification engine. It calls the same chokepoint the record-card, roster-provider and book pipelines call, because a copy-pasted gate is exactly how a leak path re-opens six months later.
Built · fail-closed, single chokepoint
record-notify.ts · isStudentSuppressedForPurpose
Recipient resolution from the enrolled roster only — never a self-asserted address
A guardian becomes a notify recipient only by being an enrolled contact on this school’s roster, carrying an address on file. An address typed into a form by whoever is holding the phone is not a recipient. This is why the platform cannot be turned into an unaudited mailing list by a well-meaning staff member: the recipient set is a projection of the roster and the consent record, and there is no writable path that adds an address to a send without adding it to the roster first, where it is visible and auditable. Every resolved recipient carries the source contact id for audit and de-duplication.
Built · roster-derived recipients only
record-notify.ts · ConsentedRecipient
Per-channel consent, quiet hours, opt-out and idempotency — the downstream walls
A student who clears the gate above is still never messaged on a channel the recipient declined. Per-channel consent, quiet-hours deferral in the recipient’s own timezone, opt-out suppression, and idempotency keyed on the message kind and subject all live in the dispatch layer and run on every send path, including the emergency broadcast path. The quiet-hours window is a real per-user record: a start hour, an end hour, and an IANA timezone that is validated on write, so a nonsense zone is rejected at the API boundary rather than silently defaulting to the server’s clock and waking a family at four in the morning. The emergency broadcast kind is deliberately DEFERRABLE rather than urgent: it respects the recipient’s opt-out and quiet-hours window. A blanket emergency bypass is a decision for the district and its counsel, and it is deliberately not wired.
Built · four independent walls, always on
notification_consent · UserNotificationPreference · URGENT_KINDS
Broadcast composition and lifecycle — a closed transition table, honest timestamps
A broadcast is composed from a reusable template that carries a subject, a body, a category and a severity. Categories are a closed set: emergency, weather, attendance, health, transportation, general. Severities are a closed set: emergency, urgent, informational. The lifecycle is a closed transition table — draft, scheduled, sending, sent, with cancellation available from draft, scheduled and sending — and the timestamps are written when the transition happens, not backfilled to make a report look tidy. The closed vocabularies are not a convention in the application code; they mirror database CHECK constraints, so a value outside the set cannot be persisted even by a caller that skipped the engine.
Built · closed vocabularies, DB-enforced
mass-notification.ts · migration 0492
Targeting — two selectors built, four deliberately unbuilt
Two audience selectors are built and run end to end: an explicit, bounded list of student references, and whole-school, which server-enumerates the roster through the existing read-only roster primitive and feeds every student through the same per-student consent chokepoint. Four selectors — role group, grade level, bus route, custom group — are shapes the store can hold and the engine will not expand. That is deliberate. An unverifiable mass fan-out is the single easiest thing in this domain to fake convincingly: it looks like it worked, it reports a plausible number, and nobody discovers the truth until the day it matters. Those four resolve to a refusal rather than to a guess, and they will stay that way until each has a bounded resolver that can prove which humans it selected.
Two selectors built · four refuse rather than guess
mass-notification.ts · AUDIENCE_KINDS
The delivery board — counts only, opaque refs, zero everywhere today
Every planned channel on every broadcast starts, and today ends, at provider-not-provisioned with zero counts and a null provider reference. That is not a display convention or an empty state waiting to be populated: it is what the engine returns, and it is pinned by a test that asserts exactly that string, exactly those zero counts. The board rolls up counts and nothing else. The per-recipient delivery surface is keyed on an opaque recipient reference — never a student id column — so no row on this surface is in the student-PII census, and the database adds a restrictive wall on top so that a report reader sees zero rows regardless. A number on this board can only ever come from a provider acknowledgement, and there is no provider.
Dark · provider-not-provisioned, zero counts, test-pinned
DISPATCH_PROVISIONED = false · mass-notification.test.ts:152
The send transport — unprovisioned, and the seam answers 503 rather than pretending
There is no outbound transport connected on this rail today. The master enablement flag is unset, which holds every lane honest-off regardless of what else is configured; and a lane whose transport is not connected stays honest-off even when every credential it needs is present, because presence of a key is not evidence of a working path. The SMS and voice lanes are additionally dark for a concrete, checkable reason: there is no per-school verified sending origin on file, so the planner suppresses each member rather than borrowing another school’s number. The dispatch route’s external processors answer 503. With no transport key the dispatch service resolves to a no-op implementation that logs and returns — it does not queue for later, it does not report a success, and it does not write a delivery row.
Unprovisioned · no transport, no queue, no fabricated ack
COMMS_LIVE unset · NoopNotificationService · no_sender_identity