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>
94 lines
4.0 KiB
Markdown
94 lines
4.0 KiB
Markdown
# 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).
|