Data model

The Advocate keeps a student's record in one document store, one collection per kind of thing, every document owned by exactly one student, so export and deletion are a single query each. Three other stores sit beside it, each with its own guarantee.

The student

erDiagram
  students {
    string id PK "stable id"
    string phone "optional"
    string chosenName
    string pronouns "optional"
    string zip
    string county "optional"
    number age "optional"
    string goal "optional"
    HelpDial dial "depth, channel, reading grade, language, quiet hours"
    Household household "size, income, children"
    Enrollment enrollment "college, credits, work hours"
    object receives "benefits already received"
    string_ helpAreas
    string_ communities "chosen, never inferred"
    string account "keyed hash of the sign-in subject"
    date createdAt
  }

The record: schedule

erDiagram
  students { string id PK  string chosenName  HelpDial dial }
  deadlines {
    string id PK
    string studentId FK "owner"
    string title
    date_or_date_time due
    string course "optional"
    enum kind "assignment, exam, reading, college, aid, benefit, bill, appointment, other"
    object source "where it was found, with the line"
    boolean confirmed
    number effortHours "optional"
    number durationMin "optional"
    boolean done "optional"
  }
  classes {
    string course
    weekday_ days
    time start
    time end
    string building "optional"
    string room "optional"
    string campus "optional"
  }
  shifts {
    date date
    time start
    time end
    string employer "optional"
  }
  holds {
    string id PK
    string deadlineId FK "optional"
    string title
    date date
    time start
    time end
  }
  reminders {
    string id PK
    string about
    date_or_date_time at
    string text
  }
  meetings {
    string id PK
    string title
    date date
    time start
    string_ attendees
    string course "optional"
  }
  courses {
    string course
    enum aiPolicy
    string aiPolicyLine "optional"
  }
  students ||--o{ deadlines : "has"
  students ||--o{ classes : "attends"
  students ||--o{ shifts : "works"
  students ||--o{ holds : "holds time"
  students ||--o{ reminders : "is reminded"
  students ||--o{ meetings : "attends"
  students ||--o{ courses : "takes"
  deadlines ||--o{ holds : "gets time in"

The record: conversation

erDiagram
  students { string id PK  string chosenName  HelpDial dial }
  drafts {
    string id PK
    string studentId FK
    string to
    string recipient
    string body
    string purpose
    enum status "shown, sent, discarded"
  }
  messages {
    string id PK
    string studentId FK
    string author "which agent"
    string text
    NextStep_ nextSteps
    enum urgency "safety, time-sensitive, routine"
    string why "optional"
    date_time at
    enum status "delivered, deferred, held"
    string holdReason "optional"
  }
  last_steps {
    NextStep_ steps
  }
  pulse_state {
    record asked
    number everyDays
    boolean off "optional"
  }
  pulse_pending {
    string pulse
  }
  dismissed {
    string subject
    date at
  }
  students ||--o{ drafts : "approves"
  students ||--o{ messages : "receives"
  students ||--o| pulse_state : "answers"
  students ||--o{ dismissed : "dismisses"

The record: help and money

erDiagram
  students { string id PK  string chosenName  HelpDial dial }
  playbook_runs {
    string playbook
    string studentId FK
    string stepId
    enum status "active, waiting, done, unresolved"
    date startedAt
    object_ history
    date waitingSince "optional"
  }
  vault {
    string name
    date addedAt
  }
  money {
    number balance
    Bill_ bills
    Paycheck pay "optional"
  }
  household_plan {
    ChildCare childCare "optional"
    number commuteMin "optional"
    number sleepHours "optional"
    enum commuteMode "bus, car, walk"
    string homeStop "transit stop id the student picked"
  }
  aid {
    date disbursement
  }
  students ||--o{ playbook_runs : "runs"
  students ||--o{ vault : "keeps"
  students ||--o| money : "tracks"
  students ||--o| household_plan : "plans"
  students ||--o| aid : "expects"

The record: connections

erDiagram
  students { string id PK  string chosenName  HelpDial dial }
  sources {
    enum source
    date connectedAt
  }
  api_tokens {
    string id PK "sha256 of the token"
    string label
    enum_ scopes "read, write, actions"
    date createdAt
  }
  webhooks {
    string id PK
    url url
    string_ events
    string secret "signs each delivery"
  }
  students ||--o{ sources : "connects"
  students ||--o{ api_tokens : "creates"
  students ||--o{ webhooks : "registers"
Collection Key What it holds
students id Who the student is and how they want to be helped. Sealed facts are never stored here.
deadlines id Every date that matters: assignments, exams, aid, bills, appointments. Found dates stay unconfirmed until the student checks them.
classes student:course:index Weekly class meetings, from the schedule or a room-change email.
shifts student:date:start Work shifts, typed by the student or sent through the API.
holds id Time the student asked the Advocate to keep for a piece of work.
reminders id Reminders the student asked for.
drafts id Messages the Advocate wrote for the student to send. Nothing leaves without the student approving the exact text.
messages id Everything the team said to the student, with why and what the tone gate decided.
last_steps (transient) student The choices on the last message, so a reply of "1" by text picks the first.
playbook_runs student:playbook Where the student is in each playbook, with every checkpoint.
pulse_state student Quick questions: which were asked when, how often, or off.
pulse_pending (transient) student A quick question waiting for a yes or no.
sources student:source What the student connected: calendar, forwarding, class site, location. Each grant is also in the consent ledger.
api_tokens sha256(token) Tokens the student created to connect other tools. Only a hash is stored.
webhooks id Where the student asked to be notified. Https only; payloads carry ids, never message text.
vault student:name Names of documents the student keeps; contents are sealed.
money student Balance, bills and pay, for spotting a short month ahead of time.
household_plan student Child care hours, commute and sleep, for the week solver.
commute_plans (transient) student date
transit_now (transient) student Live arrivals and alerts at the student's stop; used only while under ten minutes old.
dispatches idempotency key Writes the student asked for (room bookings), each sent once with an idempotency key and confirmed by looking it up.
campus_feeds (transient) campus What the operator last fetched from each campus service: dining, places, careers and transit stops. Shared, not about any student.
leases (transient) job Which operator holds which job, with a heartbeat, so two never run the same job.
meetings id Group meetings to prepare for.
courses student:course Each course and its AI policy, read from the syllabus.
dismissed student:subject Alerts the student said are fine.
aid student When financial aid is expected, for book timing.

The other stores

erDiagram
  consent_ledger { int seq PK  text at  text student_id  text kind  text subject  json detail  text prev_hash  text hash }
  consent_ledger ||--|| consent_ledger : "prev_hash chains to"
  sealed_record { text studentId PK  text wrappedKey "data key, wrapped by the master key"  json facts "each encrypted"  json grants "fact to identities" }
  evidence_event { int seq PK  text at  text kind  text subject "keyed hash"  json data }
Store Guarantee
consent ledger Every grant, revoke, approval, export and deletion, hash-chained and append-only.
sealed facts Facts only granted identities can open; encrypted with a per-student key wrapped by the master key.
evidence What the factory and the product did, pseudonymous and append-only; never message content.

How the stores relate

flowchart LR
  app[(App store<br/>documents per student)] -- every grant, approval, export, deletion --> ledger[(Consent ledger<br/>append-only, hash-chained)]
  app -. names only .-> vault[vault]
  vault -. contents .-> sealed[(Sealed facts<br/>per-student keys)]
  app -- what happened, pseudonymous --> evidence[(Evidence<br/>append-only)]
  export([Export]) --> app & ledger
  delete([Delete]) --> app & sealed
  delete -- recorded --> ledger

Verified by