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.