toolshed/docs/design-in-progress/api-classification.md
2026-08-27 21:12:31 +02:00

4.8 KiB

API Endpoint Versioning Scope (Design in Progress)

Status: decision deferred. The /api/ prefix has already been changed to /api/v1/ for every route under that prefix (inventory items, storage locations, search, friends, friend requests, groups, group invites, id mapping, tags/properties/categories/availability policies/info, files/item files/staged files, and account export/import/delete). Whether that blanket approach is the right scope, or whether versioning should be narrower, is the open question below.

Problem

A versioned URL prefix matters most where two independently-deployed servers need to agree on a wire contract: federation. An endpoint that's only ever called by a server's own frontend against its own backend is deployed in lockstep with that backend, so it has no independent version to track. Applying a uniform prefix everywhere is simpler, but it also prefixes a large number of routes that never cross a domain boundary, and skips one non-/api/ route (/auth/user/<handle>/, used to fetch a friend's profile/avatar) and the file/thumbnail serving routes (/media/...), both of which do cross a domain boundary and arguably belong in the versioned surface more than some of the routes currently in it.

One exception either way: the version/capability discovery route (GET /api/version/) has to stay unprefixed no matter which option is chosen — it's how a caller finds out which versions a server speaks in the first place, so it can't itself live behind a version segment.

Options

Option A: version every real endpoint uniformly. Every route under /api/, /auth/, /admin/, and /media/ gets the /api/v1/ prefix (except /api/version/), regardless of whether it's ever called across a domain boundary. Simple to state and apply; doesn't require maintaining a classification of which routes are federation-facing.

Option B: version only routes that actually cross a domain boundary. Only routes a server's own frontend calls against a different server (directly, or that another server calls against this one) get the /api/v1/ prefix. Everything else keeps its existing, unprefixed path. Smaller versioned surface, but requires keeping the classification below current as routes change, and means a single URL path can't ever serve both a same-domain and cross-domain purpose without the whole path being versioned.

Current classification

Verified against actual call sites (which routes are dispatched against a friend's/foreign server rather than the caller's own home server), not just against which routes are technically reachable by a non-local caller — several routes accept a broader signature-based authentication than they actually need, and one route family enforces "local caller only" itself even though its authentication layer would allow a remote identity through.

Crosses a domain boundary today:

  • POST /friendrequests/ (also called against the recipient's own server, not just the sender's)
  • GET/POST /groupinvites/ (invite delivery is posted to the invitee's own server)
  • POST /group_invites/accept/ (posted to the group's own home server)
  • GET /groups/<handle>/, DELETE /groups/<handle>/members/<id>/, POST /groups/<handle>/invites/
  • * /inventory_items/<handle>/..., * /storage_locations/<handle>/... (group-owned or friend-owned items/locations)
  • GET /search/ (fans out to every known server)
  • GET /resolve_short_id/<kind>/<owner_id>/<local_id>/
  • GET /auth/user/<handle>/ (friend profile/avatar lookup — not currently under /api/)
  • GET /media/..., GET /thumbnail/<size>/... (image bytes fetched from the owning server — not currently under /api/)

Never observed leaving the caller's own home server:

  • GET/POST /friends/, DELETE /friends/<id>/, DELETE /friendrequests/<id>/
  • GET /groups/ (the collection route, as opposed to /groups/<handle>/)
  • DELETE /groupinvites/<id>/, POST /groupinvites/<id>/accept/, GET /groupmemberships/
  • GET /idmap/ (distinct from /resolve_short_id/..., which does cross domains)
  • * /workflows/...
  • GET /tags/, GET /properties/, GET /categories/, GET /availability_policies/, GET /info/, GET /domains/ (also not currently called from the frontend at all)
  • POST /import/, GET /export/, DELETE /account_data/, DELETE /account/ — these accept the same broad authentication as the federation-facing routes, but reject any non-local caller themselves
  • GET /files/, * /item_files/..., * /staged_files/...
  • GET/PATCH /auth/user/, POST /auth/register/, POST /auth/token/, GET /auth/preferences/, GET/PUT /auth/self/preferences/, DELETE /auth/self/preferences/<key>/, * /auth/users/... (admin management)
  • everything under /admin/ (domain/category/property/tag administration)

Exception regardless of which option is chosen:

  • GET /api/version/