Subset - Docs

Read on to learn how to save, share and search with Subset.

Basics

Getting Started

  • Found something interesting on the web or in an app?
  • Tap the iOS Share icon (square with up arrow) for that item.
  • Select Subset from the row of apps to save it.
  • Add a public or private note / paste excerpts / edit the title.
  • Tap the iOS Share icon in the top left of the sheet.
  • Select elements to push to other app / tool / service.
  • Tap "Push", select the relevant target and complete the action.
  • Close by tapping "Done".
  • Later, open the main app.
  • Tap the magnifying glass on the toolbar.
  • Type a word from the saved item's title, URL, notes, excerpts or share records.

Save

  • A saved item in Subset is a unique URL.
  • There are four ways to save items to Subset:
    • Via the Share Extension on the web or within an app
    • Single: in-app, tap "+" and paste a single URL
    • Multi: in-app, tap "+" and paste text with multiple URLs
    • Fetch: in-app, tap "+" and paste a target URL to retrieve links from
  • Multi and Fetch additions initiate a review screen, including the ability to name an addition and add a note to every added item.
  • Each addition type is searchable (e.g. "src:fetch").
  • Add public and private notes to each saved item, and paste in excerpts.
  • Tap "Get title" to retrieve the title from the URL or tap "Edit title" to customise the current one.
  • "Clean URL" makes it possible to remove specific fragments and parameters from a URL (e.g. remove "#section" or "?si:sfk4334nc2").
  • In the list view, swipe actions are available for opening the link, pushing and sharing it (swipe right), as well as deleting, archiving and marking unread (swipe left).

Share

  • Subset users must connect with one another to share.
  • To do this, tap the two-people icon in the top right of the main screen.
  • First, "Edit connect alias" to something your peer will recognise.
  • Second, "Get connect code" and share it with your peer via a secure channel (e.g. WhatsApp, Signal).
  • Third, ensure your peer has received your code and vice versa; now, "Paste connect code".
  • Important: both peers must have received and pasted their codes for sharing to function.
  • At any time, you may disconnect asymmetrically from a peer, making it impossible for you to receive their shares.
  • You may also, at any time, favourite, archive or delete a peer, change their alias on your device, or merge two peers into one.
  • Once connected, select the share action (two-people) by swiping right in the list view or tapping the detail view toolbar.
  • You will then be able to choose which peers to share the item with and select the elements to include (public notes, excerpts, a unique share note).
  • Share records are automatically created for every outbound share; inbound shares will trigger a push notification and result in the creation of an additional share record (or the creation of an item if you haven't already saved it).
  • As well as peer-to-peer sharing amongst Subset users, you can also push saved items to other apps, tools and services.
  • Swipe right in the list view and select the iOS Share icon (square with up arrow) to trigger a push. Select the elements to include (public notes, excerpts, a unique share note) and complete the action.
  • Successful pushes will result in the creation of a new push record.
  • The Share Extension also allows you to link a share record to a share action.
  • To do this, go to the result of your push action on the web or in an app and open the Share Extension. Select the link icon at the top to see a list of recent pushes and choose the record that corresponds with the outcome.
  • That share record is now linked with its outcome, so you can return to it via the "..." context menu of that share record at any time.
  • Subset includes a custom, full text search engine.
  • For basic search, just tap the magnifying glass in the toolbar and type a term.
  • Field-specific search examples include "u:term" (URL), "t:term" (title), "n:term" (notes), "e:term" (excerpts).
  • Date-specific search, examples include "c:YYYY-MM-DD" (created on specific day), "m:-7d" (modified in the last 7 days), "c:>YYYY-MM" (created after specific month), "m:YYYY-MM-DD/YYYY-MM-DD" (modified between specific days)
  • Multi-term searches are implicit AND searches by default, but AND, OR, NOT and NEAR operators are available and can be combined.
  • "!" is available as shorthand for the NOT operator (e.g. "technology !AI").
  • To see the full search guide, tap the circled-''i" on the keyboard toolbar when entering a search term.
  • Searches that result in an action (detail view entry, go-to link, an admin operation, a share, a push) are automatically logged as views.
  • Searches may also be manually saved as views by tapping the viewfinder icon on the keyboard toolbar when entering a search term.
  • To see your views, tap the viewfinder icon on the main toolbar.
  • Tapping a view activates the logged query.
  • Swipe left to favourite a recent view or delete it.
  • Swipe left on a favourited view to unfavourite or delete it.
  • Favourited views can be renamed by tapping the pencil icon.
  • Favourited views can also be set to "CLOSED" state. Views are "OPEN" by default (the same query is run again on activation). "CLOSED" views, in contrast, resurface the specific results at the time the query was last executed.
  • You may activate a view and then add a search term (e.g. "v:unread t:ai").
  • You may also swipe left on an item whilst a view is active and tap the eye-cross icon to hide it from that specific view.

HTTP Peer Protocol v1

In Subset, a peer is usually a person. An HTTP peer is an endpoint instead: a URL and a bearer token that sit in the same peer list and can be shared to exactly like a person. What happens to a share after it arrives — publishing it to a site, filing it in a notebook, forwarding it somewhere else — is up to the receiver.

This section is the whole contract. A receiver built from it alone is a conforming receiver. Subset has no knowledge of any particular receiver; it holds only the URL and the token.

Status

Version 1, stable. Changes to v1 are additive only: new optional fields that receivers must already ignore (see Versioning).

Trust model

Transport security is TLS. Authentication is a bearer token, held in the sender's keychain and presented on every request. There is no payload encryption and no signing: the receiver is an endpoint the user has chosen to publish to, so content sent to it is content the user is declassifying on purpose.

This is a deliberate departure from shares between people, which are end-to-end encrypted and stay that way. An HTTP peer holds no key material belonging to the sender, and it is excluded from every broadcast path that assumes a peer can decrypt (stories, bulk share to all).

Subset only talks to https endpoints. It will not send a token over plaintext.

Revocation is the receiver's lever. A revoked token produces 401, and the sender shows the peer as needing a new token.

What is sent. Only the fields in POST leave the device: the link, its title, a one-off share note, whichever public notes and excerpts the user selected for this share, the sender's display name, and the time of sending. Private notes, unselected entries, the sender's keys, and everything else on the link's record stay local.

Setup

The user adds a receiver by pasting a setup token into Subset:

SUBSET-HTTP:<base64url(JSON)>
{ "v": 1, "url": "https://receiver.example/subset", "token": "<secret>" }
FieldTypeMeaning
vintSetup token format. Must be 1.
urlstringThe endpoint. Must be https.
tokenstringThe bearer secret. Non-empty.

Base64url padding is optional. A receiver can show this string on its own setup page, or its operator can mint one by hand:

URL="https://receiver.example/subset"
TOKEN="$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
echo "Bearer secret for your receiver: $TOKEN"
printf 'SUBSET-HTTP:%s\n' "$(printf '{"v":1,"url":"%s","token":"%s"}' "$URL" "$TOKEN" \
  | base64 | tr '+/' '-_' | tr -d '=\n')"

Configure the receiver to accept that secret, then paste the SUBSET-HTTP: line into Subset. The token is usually generated on a desktop and pasted on a phone, so offer it in a form that survives crossing devices. Treat it as a secret with a finite life, and provide a way to revoke it.

On paste, Subset GETs the endpoint and shows the name the receiver reports, so the user confirms the target by what it calls itself rather than by eyeballing a URL. The peer's alias starts as that name and stays editable; the alias is local to the sender and is never sent back.

Subset identifies a receiver by its URL, ignoring case in the scheme and host, a trailing slash, and any fragment. Pasting a new token for the same URL updates the existing peer rather than adding a second one. Two URLs that differ in path or query are two peers.

Endpoint

One URL, two methods. The receiver chooses its own path; the protocol imposes no structure on it.

Answer at exactly the URL in the setup token. Don't redirect: HTTP clients re-issue a redirected POST as a GET, and Subset will report the share as unconfirmed (see Responses).

GET <endpoint> — introspect and validate

GET /subset HTTP/1.1
Authorization: Bearer <token>
Accept: application/json
{ "ok": true, "v": 1, "name": "Example notes" }
FieldTypeMeaning
vintThe highest protocol version the receiver speaks. At least 1.
namestringWhat the receiver calls itself. Shown at setup and used as the peer's initial alias. If empty, the endpoint's host is used.

Serves three jobs: confirming the target at setup, supplying the peer's initial alias, and answering the periodic validation check. It must be cheap, must not change anything, and must be safe to call repeatedly.

POST <endpoint> — deliver a share

POST /subset HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
{
  "v": 1,
  "id": "6F9619FF-8B86-D011-B42D-00CF4FC964FF",
  "sentAt": "2026-08-04T14:22:31.482Z",
  "from": "Sam",
  "url": "https://example.com/article",
  "title": "The article's title",
  "note": "why I'm sending this",
  "publicNotes": [
    { "ts": 1754312551482, "text": "a public note on this link" }
  ],
  "excerpts": [
    { "ts": 1754312559001, "text": "a passage from the article" }
  ]
}
FieldTypeMeaning
vintProtocol version of this share. 1.
idstringSender-generated UUID, unique per share. Echoed in the response and used as the idempotency key. See Idempotency.
sentAtstringISO 8601 with fractional seconds, UTC. When the sender sent it, not when the receiver got it.
fromstringThe sender's display name. Display only: not an identity claim and not authenticated.
urlstringThe link. The only field guaranteed non-empty.
titlestringMay be empty.
notestringThe one-off share note for this share. May be empty.
publicNotesarraySelected public notes, in the order the sender listed them. May be empty.
excerptsarraySelected excerpts, same. May be empty.

ts on an entry is milliseconds since the Unix epoch, and is unique within the sender's record for that link. Treat it as opaque: use it for ordering and for recognising the same entry across two shares, nothing else.

Responses

The status code carries the meaning, and getting it right is what makes the sender's confirmation worth anything.

CodeMeaningBody
200Stored durably.{ "ok": true, "id": "<echo>", "url": "<permalink>" }
202Accepted, not yet stored.{ "ok": true, "id": "<echo>" }, no permalink
400Malformed, or unsupported v.{ "ok": false, "error": "<reason>" }
401Token missing, invalid, or revoked.{ "ok": false, "error": "<reason>" }
409Already accepted this id.{ "ok": true, "id": "<echo>", "url": "<permalink>" }
413Payload above the receiver's limit.{ "ok": false, "error": "<reason>" }
5xxReceiver's fault.any

200 and 409 must echo the share's id. Without a matching id, Subset reports the share as not confirmed rather than delivered. That is what stops some other 200 — a redirect that turned into a GET, a proxy's page — from passing for a stored share.

Don't return 200 before the share is durably stored. If the receiver queues the work, it returns 202 and no permalink. Subset records a 202 share as sent, with no link.

url is the permalink: where the share will be readable. Subset stores it as the share's outcome, which the user can open from the share's history, and ignores it unless it is an http or https URL. It is a prediction, not a URL the receiver has confirmed is live: a receiver that commits to a git repo behind a static host can answer as soon as the commit lands, without waiting for the site to rebuild.

error is shown to the user, so write it for a person.

Idempotency

id identifies one share. A receiver that has already accepted an id returns 409 with the original permalink rather than storing a second copy, and Subset treats 409 as success.

Subset does not currently retry a failed send on its own, and a share the user sends again gets a new id. The 409 rule is still required: it lets a future sender retry safely over a flaky connection. Remembering ids for 24 hours is enough.

Validation

Subset re-checks each HTTP peer with a GET when the app comes to the foreground, at most once every 30 minutes per peer. A dead endpoint or revoked token then shows up before the user tries to share, instead of at that moment. 200 is healthy, 401 marks the peer as needing a new token, and anything else marks it unreachable. Keep GET cheap.

Limits

url, title and note are short by nature. publicNotes and excerpts are not capped by count, so the practical limit is the body size.

Subset won't send a body above 256 KB. Receivers should accept at least that much, and return 413 above their own limit. Subset shows 413 as a share that was too large, and the user can send it again with fewer entries.

Versioning

  • Setup token v: a change to the token format is a new v. Subset rejects a version it doesn't know.
  • GET v: the highest protocol version the receiver speaks. A receiver keeps accepting every version it has ever advertised, so a sender on an older version keeps working after the receiver moves on.
  • POST v: the version this share is written in. Return 400 for one the receiver doesn't speak.
  • Additions within v1: new optional fields only. Receivers ignore unknown fields in a share, and senders ignore unknown fields in a response.

Receiver checklist

  1. GET with a valid token returns { ok, v, name }, with v ≥ 1. Otherwise it returns 401.
  2. The endpoint is https and answers at exactly the setup URL, with no redirect.
  3. POST authenticates before parsing.
  4. POST rejects an unsupported v with 400.
  5. POST stores durably before answering 200, or answers 202.
  6. 200 and 409 echo the share's id. url, when present, is http(s).
  7. A repeated id returns 409 with the first permalink.
  8. Unknown fields are ignored, not rejected.
  9. Bodies up to 256 KB are accepted, or refused with 413.
  10. The token can be revoked, and revocation shows up as 401.