Spec from the mobile side, v2. One endpoint (POST /api/sendoff/send — the one sanctioned door into another user's filesystem, sender identity from the token), a push category whose route deep-links the Files apps, and a minimal devices registry for the send-to pickers. The mobile half is already shipped against this and degrades silently until it exists. Lives here rather than in COMMS because it is a spec to implement, not a message: the repo is where it can be corrected as the implementation teaches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.0 KiB
SendOff — files land in the user's own tree, and a push says where
From: mobile · Date: 2026-08-23 · v2, replacing this morning's spec entirely. The v1 design (staging area, transfer/target/file tables, ack lifecycle, TTL cleanup) is withdrawn — Andre's observation kills it: every user already has a filesystem on the server, a file browser on the web, and a Files app on the phone. SendOff is not a new storage system; it is a folder convention, one endpoint, and a push.
The convention
~/Downloads/sendoff/from-<username>/ ← a file another USER sent you
~/Downloads/sendoff/<machine>/ ← a file you sent yourself, aimed at a named device
That is the whole data model. Nothing to ack, nothing to expire, nothing to clean up — the files are ordinary files in the recipient's tree, visible in every file UI that already exists, managed like anything else they own.
The endpoint
POST /api/sendoff/send
multipart: any number of `file` parts + a `meta` part:
{ "toUser": "<username>" } — cross-user: writes into THEIR tree, from-<sender>/
{ "machine": "<device name>" } — self: writes into own tree, <machine>/
→ { path: "Downloads/sendoff/…", files: [{ name, size }] }
Server-side because of the cross-user case: /api/file-browser/upload correctly writes only the
caller's own tree, and that must not change. This endpoint is the one sanctioned door into another
user's filesystem, and it writes ONLY under Downloads/sendoff/from-<sender>/ — the sender's
identity comes from their token, never from the payload, so the folder name cannot lie about who
sent it. Filenames sanitised, collisions suffixed (1) — never overwritten; a second send of the
same name is a second file.
Whether user A may send to user B at all is a platform policy decision (everyone on the server? a grant?) — flagging it rather than deciding it here.
The push
On write, push to the recipient's devices through the existing pipeline:
{ "officer": { "c": "officer/core:sendoff.landed", "path": "Downloads/sendoff/from-andre" } }
alert: "photo.jpg from andre" / "3 files from omega"
The push catalogue's route for the category:
officer://files?path={path}
Mobile opens routes from the catalogue verbatim (openNotificationRoute), and Officer now parses
officer://files?path=… into the Files app at that folder. The web can deep-link its file browser
the same way.
For a machine-targeted send, push only to that machine's device if it can be identified; to all
the user's devices otherwise — the path in the folder name already says who it was for.
Devices
The machine name needs a picker on the sending side, so a minimal registry survives from v1:
devices: id (client uuid), user_id, name, platform, last_seen_at
POST /api/devices/register { id, name, platform } (upsert; called on every app start)
GET /api/devices → { devices } (the "send to my…" picker)
PATCH /api/devices/:id { name } · DELETE /api/devices/:id
No link to transfers — nothing references a device row except the picker and, best-effort, push targeting.
Web UI
One send form: files + target (one of my machines / another user), POST, done. Received files need no UI at all — they are in the file browser already.
What mobile already does (shipped against this)
- Registers the device on start (silent failure until the endpoint exists).
- Parses
officer://files?path=…and opens the Files app at that path — this is the piece that makes the push tap land somewhere. - SendOff screen: the device's name (the folder senders will aim at), and an explanation of where arrivals land. Sending FROM the phone comes later, likely through the existing share sheet.
Out of scope
Receiving in the browser (already exists — it is the file browser); cross-user permission model (platform decision); phone-side arbitrary-file sending (needs a document picker; the share sheet is the natural door and is a later step).