This document is the boundary decided in ADR 0011: the interface that premium features, integrations, and any third-party tool may build on — all with exactly the same access. If it isn't described here, it isn't contract, and building on it is at your own risk.
TrenchNote's API is PocketBase's standard REST
API — the core adds collections
and rules, not custom endpoints. Everything below is reachable at the same
origin that serves the pages (e.g. http://192.168.1.50:8090).
These fourteen collections — their fields, semantics, and the operations marked allowed — are stable. A breaking change to any of them requires a new ADR and a version bump of this contract, announced in the release notes.
| Collection | Read (list/view) | Create | Update | Delete |
|---|---|---|---|---|
items |
✔ contract | ✔ | ✔ | admin-only |
locations |
✔ contract | ✔ | ✔ | admin-only |
assets |
✔ contract | ✔ | ✔ (see cache rule) | admin-only |
movements |
✔ contract | ✔ (see shape rule) | never | never |
reservations |
✔ contract | ✔ (open only) | ✔ (lifecycle) | admin-only |
readings |
✔ contract | ✔ | never | never |
inspection_requirements |
✔ contract | ✔ | ✔ | admin-only |
inspections |
✔ contract | ✔ (see asset-match rule) | never | never |
condition_reports |
✔ contract | ✔ (photo required) | never | never |
condition_resolutions |
✔ contract | ✔ | never | never |
manifests |
✔ contract | ✔ (draft only) | ✔ (forward workflow only) | admin-only |
manifest_lines |
✔ contract | ✔ (draft parent) | ✔ (receipt fields only) | ✔ (draft parent only) |
container_events |
✔ contract | ✔ | never | never |
kit_audits |
✔ contract | ✔ | never | never |
Field-level shapes are defined by the migrations in pb_migrations/ and
explained in the developer guide.
Highlights that are load-bearing for API clients:
movementsis an append-only ledger (ADR 0002). No client — premium, third-party, or future core code — can ever update or delete a movement. Corrections are new records.- Receiving-log fields on movements (ADR 0013, additive):
vendor_name,po_number,osd_note(all free text),packing_slip(file, max 1) andphotos(file, max 8) — dispute evidence meant for receive-shaped bulk movements (deliveries), though the create rule does not forbid them elsewhere. All optional; existing clients are unaffected.po_numberis what a human typed at the truck — the API has no concept of purchase orders or ordered quantities, by design. Creating a movement with files requires amultipart/form-dataPOST (PocketBase standard); JSON creates work unchanged when no files are attached. - The movement shape rule (enforced server-side; malformed records are
rejected no matter who sends them). A movement is exactly one of:
- Asset move:
assetset,itemempty,quantity = 0,to_locationrequired — a physical machine always lands somewhere. - Bulk move:
assetempty,itemset,quantity > 0, and at least one offrom_location/to_locationset. Which are set determines the meaning (ADR 0005): receive =toonly; transfer = both; consume =fromonly (installed / used up — leaves stock without landing anywhere, record stays in the ledger forever).
- Asset move:
assets.current_locationis a cache, not truth. Clients that log an asset move must write the movement record first, then PATCH the cache — in that order, always.- Bulk stock is derived, never stored. Compute stock-on-hand per location by summing bulk movements (in minus out; consumes subtract and add nowhere). There is no stock column, and there never will be (ADR 0002).
- Reservation lifecycle (ADR 0007):
statusis one ofopen | fulfilled | cancelled, stored, not derived. Empty status means open — records created before the status field exist and are read as open everywhere; clients must treat""and"open"identically. The create rule rejects reservations bornfulfilledorcancelled.noteis free text. Fulfilled/cancelled claims remain in the database as demand history. tag_codeis permanent once printed on a label (ADR 0010): unique, never recycled onto different gear. QR labels encode{Base URL}/asset.html?code={tag_code}.- Rental dates (ADR 0015):
assets.on_rent_dateandassets.off_rent_dateare optionaldatefields (empty on owned gear), stored date-only at UTC midnight — format in UTC. Dates are a non-commercial fact about the asset and live in core; the rate does not (that stays with whatever tool prices the rental). Added in migration1783468816; additive to contract v1 (see Versioning). items.item_code(ADR 0018): optional, non-unique text — the office's catalog/reference number for a kind of thing (e.g. a bulk item's "Misc-66"), distinct from an asset's uniquetag_code. Migration1783468818; additive.readingsis an append-only ledger (ADR 0012), same rules as movements: no updates, no deletes, corrections are new records. A reading isasset+value+reading_type(hours|odometer) + optionalrecorded_by(free text),photo(the gauge), andread_at(ADR 0016 — the observation date, date-only UTC midnight, empty = fall back tocreated; set at capture so offline readings keep their real read-day).createdstill records system-entry time. Latest reading is derived (newest record per asset — there is no latest-reading column on assets); derivation now orders byread_at, falling back tocreated— a refinement clients should mirror (sort=-read_at,-created). A value lower than its predecessor is legal data (replaced meter or typo) that consumers should flag, not drop.- Billing facts on locations (ADR 0012):
job_codeis the accounting job number equipment time at that location is billed to; an asset's "current job" is derived as its current location'sjob_codeand is deliberately stored nowhere.notify_email, when set, makes the core email that address whenever a movement with a destination leaves the location — best-effort via the instance's SMTP settings, server-side. API clients get no delivery feedback and must not depend on it; a mail failure never fails the movement create. items.meter(hours|odometer, empty = no meter) drives whether the UI offers a reading at scan time;assets.assigned_tois free-text custodianship. Both optional, both plain facts with no side effects.inspectionsis an append-only ledger (ADR 0014), same rules as movements and readings: no updates, no deletes, corrections are new records. An inspection isasset+result(pass|fail|removed_from_service) + client-setinspected_at(date-only at UTC midnight — unlike the other ledgers, the compliance-relevant date is supplied by the client so offline capture and back-entered paper records keep their true dates;createdstill records entry time), plus optionalrequirement(relation; empty = ad-hoc),inspected_by(free text),note,photo. Asset-match rule, enforced server-side:requirement, when set, must belong to the sameasset— a mismatched pair is rejected no matter who sends it.inspection_requirements(ADR 0014) is catalog-like data:asset(relation),name,interval_days(positive integer),reference(free text). Any signed-in client may create and update; delete is admin-only. All compliance status is derived, never stored: next-due = the requirement's latest passing inspection'sinspected_at+interval_days; there is no status column anywhere, and there never will be (ADR 0002). Consumers rendering status should treat a requirement with no passing inspection as not current, and an asset whose latest inspection on any requirement isfail/removed_from_serviceas out of service.- Damage and condition evidence (ADR 0019) is split across two
append-only ledgers.
condition_reportsisasset+report_type(damage|wear|condition_note) + requireddescription, requiredphoto(one image), and required free-textreported_by;createdis the server timestamp.condition_resolutionsisreport+resolution(repaired|accepted_as_is|disposed|returned_to_vendor) + optionalnote+ required free-textresolved_by. Neither collection permits update or delete by an ordinary authenticated client. DAMAGED is derived, never stored: a damage report is open when no resolution references it; an asset is damaged when any of its damage reports is open. Wear and condition-note reports do not affect that badge. Multiple resolution rows are legal; any one closes the report for the derivation while all remain in history. - Transfer manifests (ADR 0020):
manifestsrecordsfrom_location,to_location, authenticatedcreated_by, free-textdriver_name, forward-onlystatus, and authenticatedreceived_by. Allowed transitions aredraft → in_transit → receivedorreceived_with_discrepancies; API rules reject backward transitions and require the receiving account to equal@request.auth.id.manifest_linesis exactly one asset, or one bulk item plus positivequantity;sent_quantityfreezes at dispatch. While in transit, onlyreceived_quantity(0 through sent) andcondition_notemay change. PocketBase number fields use zero as their empty value, so parent status distinguishes an unconfirmed zero from “receiver confirmed none.” - Transit and shortfall semantics: dispatch writes no movement and creates
no virtual location; in transit is derived from the parent manifest status.
Confirmation writes ordinary movements. Received quantity goes from source
to destination and any shortfall goes to the seeded
Missing in transferlocation (tnmissingxfer01, typetransit). Existing movement/stock semantics remain unchanged.
- Query features: PocketBase's
filter,sort,expand(including nested expands likeasset.item), and pagination (page,perPage, max 500 per page) on the collections above. - File URLs:
/api/files/{collection}/{recordId}/{filename}— e.g. an item's photo at/api/files/items/{id}/{photo}. - Realtime: PocketBase's SSE subscriptions ("server-sent events" — a
one-way stream the server pushes changes over) at
/api/realtime, for the contract collections, authenticated like everything else. Prefer this over tight polling loops; the server may be a Raspberry Pi whose first job is serving field scans. - Transactional batch:
POST /api/batchis enabled for manifest dispatch and receipt (maximum 250 subrequests, 10-second server timeout). The normal per-collection rules above apply to every subrequest; the whole request commits or rolls back as one PocketBase/SQLite transaction.
The internal SQLite file layout and pb_data/ contents, the PocketBase
admin UI and admin-only endpoints, PocketBase system collections, and any
endpoint or behavior not listed here. These can change without notice.
Every operation above requires a signed-in user (ADR 0004, migration
1783468806). Anonymous access returns nothing; public self-signup is
disabled, so accounts are created by the admin in the PocketBase UI.
- API clients authenticate as a service account: an ordinary PocketBase user record with its own credentials and only the permissions any authenticated client gets — never a superuser key. Give each integration its own account so access can be revoked independently of field phones.
- Obtain a token via PocketBase's standard
auth-with-password, send it as theAuthorizationheader, and renew it withauth-refresh. - Gotcha (contract-relevant): PocketBase treats a missing or invalid
token on reads as a guest and returns empty lists with HTTP 200,
not 401. Detect auth failure via
auth-refresh, not response codes — otherwise an expired token is indistinguishable from an empty jobsite.
- This is contract v1. Additive changes (new optional fields, new collections) don't bump the version; breaking changes (renamed/removed fields, changed rules, changed URL patterns) require an ADR and bump this document's version, announced in release notes.
- Contract v1 as published here reflects the schema through migration
1783468824(readings — ADR 0012; certs & inspections — ADR 0014; receiving log — ADR 0013; rental dates — ADR 0015; reading observation date — ADR 0016; item code — ADR 0018; damage & condition reports — ADR 0019; transfer manifests — ADR 0020; all additive: new collections and optional fields only; Gang Box fields/events/audits follow in migrations1783468822–1783468824). - Core is currently developed and tested against PocketBase 0.39.x
(pin with
PB_VERSION=0.39.6 ./scripts/setup.sh). A PocketBase upgrade that changes REST behavior is treated as a breaking change and handled the same way.