Working system · District approved · Not yet deployed

Classroom Narrative AI

Privacy-first K–12 narrative infrastructure: teachers turn classroom evidence and standards performance into report-card narratives — built on the assumption that data about minors is never ordinary application data.

DISTRICT APPROVED · EXTERNAL VALIDATION
Approved for use in a Mountain View school district

The district is not named here because public attribution has not been granted — withholding it is the same identity-minimization rule the product itself is built on. The approval evidence is held privately; the design rules that came out of the district review are dated 2026-08-30 and are documented verbatim in the product source (lib/privacy/alias.ts). Approval means approved for use. It does not mean piloted, deployed, or in production — those stages have their own rows below, and they are honest.

BUILD · working system REVIEW · completed APPROVAL · approved for use PILOT · not started DEPLOYMENT · not deployed

The problem

Narrative report cards take teachers hours per student, three times a year: gather the evidence, map it to standards, find words that are accurate and kind. Generic AI tools make the writing faster and the privacy worse — they want the one thing a teacher must not hand over: who the student is.

Identity architecture

Real identity never leaves the teacher’s device. Everything off-device operates on a randomized alias.

Teacher device · roster Student name / ID (local only) Local mapping Randomized alias · S-K7MPQ2XW Application Evidence & history Narrative Teacher review Approved export
/** lib/privacy/alias.ts — verbatim from the product source */
 * Every student gets one cryptographically random code (e.g. "S-K7MPQ2XW")
 * that stands in for them everywhere off this device. Design rules, from the
 * Mountain View district review (2026-08-30):
 *
 *  - Random, never derived. The alias must carry zero information about the
 *    student: not a name fragment, not a hash of the SIS ID, not a counter.
 *  - Stable for the school year, so T1 -> T2 -> T3 records connect on the
 *    server without a name or SIS ID.
 *  - The mapping (alias <-> real name) lives ONLY where the roster lives —
 *    on this device (or in memory in No-Save). It is never synced.
 *
 * Honesty note: aliased records are pseudonymous, not "de-identified" — a
 * comment's content can still describe a recognizable student. Nothing in
 * this file or the marketing may claim otherwise. */
const ALPHABET = "23456789BCDFGHJKMNPQRSTVWXZ";  // no vowels, no 0/O/1/I/L

The words are chosen carefully on purpose: pseudonymous, randomized alias, identity minimization. The source forbids claiming “de-identified” because narrative content can still describe a recognizable student. A PII guard (lib/privacy/piiGuard.ts) additionally flags emails, honorific+surname patterns, and name-shaped words before anything is shared — tuned to over-flag rather than miss.

Persistence: three honest modes

NO SAVE · default

Student-bearing data lives in memory for the session only. Nothing touches localStorage, sessionStorage, or IndexedDB; refresh clears it.

DEVICE ONLY

Explicit opt-in. Data persists in this browser only and is never sent to a server.

SAVED HISTORY

Signed-in school account: server storage scoped by row-level security, holding aliases — never names, student IDs, SIS IDs, or emails — with retention and deletion controls.

Standards & evidence

Narratives are composed from structure, not vibes: the California standards tree, per-skill performance levels (growth / developing / proficient / advanced), fluency WPM where relevant, and per-period ratings that connect T1 → T2 → T3 through the stable alias — so growth-over-time works without the server ever learning a name.

Tenant security & audit

Row-level security, forced

Every student and assessment row is keyed to the teacher who created it. RLS is enabled and forced with a separate policy per operation, and blanket role grants are revoked — Postgres is the only door, not app code that could be bypassed. School workspaces add an organization boundary on top.

Append-only audit history

School-workspace actions write to an audit log carrying tenant, actor, action, and entity. The isolation script in the repo proves the properties as executable assertions:

-- db/tenant-isolation.sql — the runnable proof script (excerpt)
-- ==== As USER A (owner tenant): audit immutability + lifecycle ====
update public.audit_log set action = 'tampered'
  where org_id = '...aaaa';
-- asserts: 'audit UPDATE affects 0 rows (immutable)' -> true

API & agents

A real, live, scoped API — not a diagram. The OpenAPI 3.1 contract is served at /api/v1/openapi and matches the deployed routes. Requests without a key get 401; scopes are least-privilege (standards:read, comments:generate, exports:create…), and delegated agent calls carry masked on-behalf-of attribution into the audit trail.

The identity contract holds at the API boundary too — calls use the alias, and the generated text uses it as a placeholder the teacher re-substitutes locally:

$ curl -H "Authorization: Bearer $KEY" -d '{
    "mode":"standards",
    "student":{"alias":"S-K7MPQ2XW","pronoun":"she"},
    "scores":{"rf_phonics":"proficient"},"languages":["en"]}' \
  https://classroom-narrative-ai.vercel.app/api/v1/comments

"S-K7MPQ2XW demonstrates strong, dependable skills in Phonics
 and Word Recognition. It is a genuine pleasure to teach
 S-K7MPQ2XW, and I look forward to seeing her continue to grow."

Testing

336 checks across 26 files — unit, end-to-end, and accessibility suites — re-run today, all green. That includes the new API alias-contract tests: a valid alias generates, a name-shaped or lookalike-character alias is rejected at validation.

What broke, and what the design does about it

The contract said live; production said 404

The marketing page advertised a live OpenAPI document while the production deployment — correctly failing closed with no keys configured — hid it. Caught in this review. Fixed by provisioning scoped API keys, so the contract is now genuinely live and every route still authenticates. The fail-closed behaviour itself was right: a cleared env var must never silently open an endpoint that accepts student rosters.

Over-flagging as a choice

The PII guard matches any name-shaped capitalized word, which flags some false positives. That is deliberate — documented in the source — because the cost of missing a student name is not symmetric with the cost of a spurious warning.

The word “de-identified” is banned

The alias module’s honesty note forbids the product and its marketing from claiming de-identification, because narrative text can describe a recognizable student. Precision beats the stronger word.

Maturity, exactly

Build stageworking_system
External reviewcompleted
Approvalapproved_for_use — “Approved for use in a Mountain View school district”
Pilotnot_started
Deploymentnot deployed

These rows only ever move on explicit evidence. Approval does not imply a pilot; a pilot would not imply production. When those stages actually happen, they get their own dated entries — the history is append-only.

Daniel’s contribution

Product discovery with working teachers, the privacy architecture above, the deterministic narrative engine, the standards catalog, the scoped API and OpenAPI contract, the RLS and audit model, the test suites, and the deployment. Built end to end; the district review shaped the identity rules, and those rules are now enforced in code rather than promised in copy.

See it yourself

Open the working system Live OpenAPI 3.1 Book a walkthrough

The demo opens in No Save mode — the same default a district reviewer saw: nothing student-bearing persists unless a teacher explicitly chooses otherwise.