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.
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.
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.
/** 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
Student-bearing data lives in memory for the session only. Nothing touches localStorage, sessionStorage, or IndexedDB; refresh clears it.
Explicit opt-in. Data persists in this browser only and is never sent to a server.
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)' -> trueAPI & 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 stage | working_system |
| External review | completed |
| Approval | approved_for_use — “Approved for use in a Mountain View school district” |
| Pilot | not_started |
| Deployment | not 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
The demo opens in No Save mode — the same default a district reviewer saw: nothing student-bearing persists unless a teacher explicitly chooses otherwise.