Skip to content

Data model

What the server stores for each account, and what the API and the export file carry. The user-facing description of the export file is in Export file formats.

status/<key> — a decision on a form line

{"s": "used", "kids": ["a", "b"], "note": "Netflix, Disney+", "path": "Instructional Material › …", "at": "2026-10-01T12:00:00.000Z"}
Field Values
s routine, used, check, no, or null (no status, only students or a note).
kids Student ids the line applies to.
note Free text.
path The line's full name, for readability in exports.
at Last change, ISO 8601. Used to pick the newer side when merging.

Key: the first 8 hex characters of the SUFS category id; then - and the type id's first 8; then - and the detail id's first 8. For example 1035f351 is a category, 1035f351-9c2d41aa would be a type under it and 1035f351-9c2d41aa-0b77e9f3 a detail. A status on a category or type is inherited by every line under it that has no status of its own.

log/<id> — an expense

{"date": "2026-03-04", "year": "2025-26", "vendor": "Netflix", "desc": "Streaming plan", "amount": 15.49,
 "extra": 1.05, "status": "todo", "path": "1035f351-9c2d41aa", "pathText": "Instructional Material › …",
 "kids": ["a"], "note": "Invoice 12", "repeatOf": "e1abc", "at": "2026-10-01T12:00:00.000Z"}
Field Values
date Purchase or service date, YYYY-MM-DD; empty while unknown.
year School year it counts toward, e.g. 2025-26 (July to June). Empty while the date is unknown.
vendor Store or provider; empty while unknown.
invoice The receipt's number, as the SUFS form asks for it (Invoice/Receipt #; an online order number counts). Absent when not known.
desc What it was.
amount Dollars paid, in total; null while unknown. With items, the sum of the items' totals (and null while any item's price is unknown).
extra Optional "tax, shipping, etc." part of amount (sales tax, shipping, delivery and similar charges, as one amount, the way the SUFS form asks for them); with items, their sum. Absent when not broken out. The API also accepts tax and shipping on an expense or item and stores their sum as extra.
items The receipt's lines claimed, each on its own form line (below). Absent on expenses saved before items existed, which count as one item made of path, amount and extra. When present, path/pathText are the first item's and amount/extra are derived from them by the page.
status Where the SUFS request stands: todo (to submit), draft, submitted, inreview, changes (SUFS asks for changes), completed. SUFS's decision is per item (items[].status). Records with approved, paid or denied here come from before outcomes were per item: they read as completed with that outcome on every item that has none, and the API stores them that way.
sufs {id, status, seen} when the expense is linked to a SUFS request: the request's id, the status last taken from it, and a fingerprint of the request's and its lines' statuses at that moment. Whenever SUFS's data no longer matches seen, the page sets the expense's status from the request, each item's outcome from its line, and fills item fields that are still empty from the line; a status set by hand stays until SUFS changes again. Absent when not linked.
path Key of the form line, or null if not linked.
pathText The line's full name, or whatever was typed if unlinked.
kids The student it was for, as a one-element list. An expense is for one student; one bought for two is two expenses. An imported entry holding several ids is flagged in the page until it's duplicated per student.
note Free text.
repeatOf Id of the entry this was created from by Repeat (absent otherwise).
at Last change.

Ids are generated by the page (e + base-36 time + random suffix).

An item (items[]), shaped like an item on the SUFS reimbursement form:

Field Values
path, pathText Key of the item's form line (or null) and its full name or the typed text.
qty Quantity, default 1.
cost Price each, before tax; null while unknown.
extra This item's tax, shipping, etc.; null when none. Item total = qty × cost + extra.
desc What this item was (optional).
vendor When this item came from a different vendor than the expense (optional).
invoice When this item is on a different receipt than the expense's invoice (optional; one file holding several receipts).
benefit "How will this item/service help your student learn?" (optional, ≤ 500 characters).
url The item's web address (optional).
status SUFS's decision on this item: approved, paid or denied; absent until decided.
sufsLine The SUFS line this item was submitted as ({request number}-{n}), when the expense is linked to a request.

meta/settings — the students

{"kids": [{"id": "s1", "name": "Maya", "birthYear": 2012, "program": "fesua", "sufsId": "…"},
          {"id": "s2", "name": "Noa", "birthYear": 2018, "program": "ftc"}]}

program is fesua, feseo, ftc or ftcpep. sufsId (optional) is the student's id in the SUFS portal, set when the user matches students to the requests the browser extension read. Any number of students; a new tracker starts with none, and the page asks you to add them.

file/<id> — an attached file

{"name": "amazon-order-114.pdf", "type": "application/pdf", "size": 48213, "kind": "receipt",
 "logIds": ["e1abc"], "kids": [], "year": "2025-26", "note": "", "at": "2026-10-04T12:00:00.000Z"}
Field Values
name The file's name as attached.
type Its media type (application/pdf, image/jpeg, …). Only the types listed under Configuration are accepted.
size Bytes.
kind receipt (receipt or paid invoice: what SUFS asks for), supporting (anything else handed in with a claim), internal (kept for the family only, never part of a claim), unsorted (added in bulk, not organized yet).
logIds The expenses this file documents. One receipt can cover several entries (one per student, say).
kids, year For internal reports: the student(s) and school year it is about.
note Free text.
text The file's text as the page read it (PDF text layer or OCR), up to 50 000 characters. Absent until read; "" when nothing could be read.
inferred {vendor, path, date, amount, tax, shipping, subtotal, discount, mismatch, paid, items, invoice, found}: what the page guessed from the text. items is [{desc, qty, cost}] (cost = price each), present only when the receipt's item lines add up to its subtotal (or total less tax and shipping), else null. tax and shipping stay apart here because checking the total needs both; an expense filled from them gets their sum. mismatch is a sentence when the stated parts don't make the total. null when nothing was read.
readError Why reading failed, when it did.
at Last change.

Id: the first 32 hex digits of the SHA-256 of the file's contents, computed by the page. The same contents attached twice are one file, and the record can exist on a device that doesn't hold the bytes (the page then says not on this device). The record is the document; the bytes are stored separately under the same id and are uploaded before the record is saved (API). Deleting the record deletes the bytes.

sufs/<id> — a SUFS reimbursement request

Written by the browser extension from the portal's own data (the id is the request's id in the portal); the page only reads them.

{"number": "10000001", "submitDate": "2026-03-02", "originalSubmitDate": "2026-03-02", "schoolYear": "2025-2026", "program": "FES-UA", "provider": "Amazon",
 "studentId": "…", "studentName": "…", "status": "In Review", "total": 21.4, "resubmitted": false, "appealed": false,
 "lines": [{"number": "10000001-1", "category": "…", "type": "…", "detail": "…", "qty": 1, "cost": 20, "additional": 1.4,
            "amount": 21.4, "vendor": "Amazon", "purchaseDate": "2026-03-01", "serviceDate": null, "invoiceDate": null, "invoiceNumber": null, "status": "Submitted",
            "reasonCode": null, "reason": null, "comment": null, "commentType": null, "approvedDate": null, "paymentDate": null,
            "benefit": "…", "url": null}],
 "reasons": [{"line": "10000001-1", "reason": "…", "comment": null, "at": "2026-04-02T10:00:00Z"}],
 "history": [{"at": "2026-03-05T10:00:00Z", "line": null, "status": "Submitted", "reason": null, "comment": null}, …],
 "readAt": "2026-10-05T10:00:00Z"}

lines are the request's items as SUFS has them (category names are the form's); reason is SUFS's reason for a line's status and comment the reviewer's comment, as SUFS's request list carries them. reasons keeps every change-request reason or comment ever seen on a line, with when it was first seen, since SUFS clears them once the request is edited. history is the request's change log: an entry each time the request's status, or a line's status, reason or comment, differed from what was last recorded for it (line is null for the request itself), starting with the state the first reading found. readAt is the reading it comes from. trackerLog (optional) is the expense the extension's submit helper filled this request from; the page links the two unless either is already linked.

In the database

Each account's documents are rows in the status, log, meta, file and sufs tables, keyed by account; log_fts and status_fts are full-text indexes over the first two. Accounts, users, memberships, sessions, invitations and API tokens have their own tables. The schema carries a version number and is upgraded in place on startup.

File contents are not in the database: they are plain files under SUFS_FILES_DIR/<account id>/<file id> (/data/files in the container), written once and removed with their record or their account. Files there that no record points at are removed when the server starts.