diff --git a/docs/sendoff.md b/docs/sendoff.md new file mode 100644 index 00000000..2d00f118 --- /dev/null +++ b/docs/sendoff.md @@ -0,0 +1,93 @@ +# 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-/ ← a file another USER sent you +~/Downloads/sendoff// ← 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": "" } — cross-user: writes into THEIR tree, from-/ + { "machine": "" } — self: writes into own tree, / + → { 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-/` — 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).