Skip to content

User guide ​

A guide for people using Patches — running the patches terminal client and connecting to a node. For contributor/developer setup, see the root README.md and docs/operations/local-development.md; for the wire protocol, see docs/architecture/api.md.

What Patches is ​

Patches is a small, chronological, open-source social network whose first-class client is a terminal app. Posts, replies, likes, bookmarks, follows, and a personal "Patches Page" (a declarative profile page with text, links, a guestbook, and — once media is attached — inline images), all sorted by time. No ranking algorithm, no ads, no infinite scroll. A node is a server one or more people run; you connect the patches client to whichever node your account lives on.

Installing ​

Status: planned until first publish.

bash
npm i -g patches-social
patches --version

apps/tui now builds as a single self-contained bundle (apps/tui/tsup.config.ts) with the three workspace packages it needs (@patches/domain, @patches/proto, @patches/terminal-media) inlined directly into dist/cli.js, so a plain npm install -g no longer needs those packages published on their own — see apps/tui/README.md's "Self-contained build" section for how. Verified locally end-to-end (2026-08-18): building, packing, and installing the tarball into a scratch global prefix produces a working patches binary that runs --version, --help, and ping against the live node with no repo checkout on PATH. What's still outstanding is the publish itself (npm login + pnpm publish, a manual step by the package owner — see docs/operations/deployment.md's "Publishing the TUI" section), so npm i -g patches-social isn't runnable from the public registry yet.

Until it's published, run the client from a source checkout:

bash
git clone <repo-url> patches && cd patches
mise install        # installs the pinned Node 24 / pnpm 11 / buf toolchain (mise.toml)
mise run setup       # pnpm install, .env, local Postgres+mailpit via compose, migrations, build
mise run tui         # builds and runs the TUI against a local server on 127.0.0.1:50051

mise run tui (see mise.toml) is a convenience wrapper around building @patches/tui and running node apps/tui/dist/cli.js --server 127.0.0.1:50051 --insecure. mise install and mise run setup are the only two commands needed for a from-scratch checkout; both are defined in the repo's mise.toml and were run to verify this guide.

If you already have the toolchain and just want to build/run the client by hand:

bash
pnpm --filter @patches/tui build
node apps/tui/dist/cli.js --help

Connecting to a node ​

The client's default is the flagship node, patches-social.fly.dev:443, over TLS. Run patches (or patches ping) with no flags and you're talking to it — no --server, no --insecure. It's invite-only today (INVITE_ONLY=true); get a code from an existing member to register.

Every subcommand and the interactive TUI itself also take a --server/--node <host:port> flag (or the PATCHES_SERVER environment variable) to point at a different node instead — most commonly a locally-run dev server. Local dev servers don't have a real TLS certificate, so also pass --insecure (or set PATCHES_INSECURE=1) to connect over plaintext gRPC:

bash
patches                                                # open the TUI against the flagship node
patches ping                                           # one-shot connectivity check, JSON out, exit 0/1

patches --server 127.0.0.1:50051 --insecure            # open the TUI against a local dev server
patches ping --server 127.0.0.1:50051 --insecure       # same, for local dev

Sessions are stored per node: patches accounts lists every (node, account) pair with a stored session on this machine, and a token is never sent to a node other than the one that issued it.

Live-node status and caveats ​

Verified against patches-social.fly.dev on 2026-08-18: register, login, whoami, posting, search, follow, like, reply, thread view, notifications, and home feed all work end to end. R2 image storage and the verified Resend sender were configured after that original smoke. No new end-to-end live smoke was run for this documentation update. Federation remains disabled by design for v0 (FEDERATION_ENABLED=false), so this node talks only to itself. The hosted web footer still reports the stale 0.1.0+29df763 revision; repository fixes for web sign-in BigInt serialization and profile diagnostics are not live until B-063 deploys and smoke-tests them.

Creating an account and signing in ​

bash
patches register --server <host:port>   # prompts interactively for anything not passed as a flag
patches login --password --server <host:port>
patches login --ssh --server <host:port>
  • patches register supports --email, --handle, --display-name, --invite <code> (if the node requires invites), --password-stdin (read the password from stdin instead of an interactive prompt), and --ssh-key <path|fingerprint> to also enroll an SSH key as a login credential during registration. Email is optional recovery/verification data, not your account identifier, unless the node's policy requires it.
  • patches login --password --email-or-handle <value> signs in with a handle or recovery email plus a password.
  • patches login --ssh --ssh-key <path|fingerprint> signs in via a challenge/response against a key already loaded in your SSH agent — Patches never reads or transmits your private key, only a locally-computed signature over a server-issued challenge.
  • Signing in with a GitHub account is supported by the server (OAuth device flow), but there is no patches login flag for it yet — that client-side wiring hasn't landed.
  • patches logout (add --all to sign out of every stored account on this machine, or --user <id> to disambiguate when more than one account is stored for the same node).
  • patches whoami prints who you're currently signed in as; patches accounts lists every stored account.
  • patches keys add [--ssh-key <path|fingerprint>] [--label <text>] [--yes] enrolls an additional SSH key on your account (requires an explicit y confirmation, or --yes non-interactively); patches keys list lists your credentials (never a secret); patches keys remove <fingerprint> revokes one — the server refuses to revoke your last remaining credential, so you can never lock yourself out.
  • Email verification: use patches verify <code> to redeem a delivered code or patches verify --resend while authenticated. The accounts screen also offers r to resend while the current email remains unverified.
  • Editing your profile: use patches profile edit for the headless path, or press e on your own profile in the TUI to edit display name, bio, location, website, and nameplate.

Using the TUI ​

Once signed in, running patches (with --server/--insecure as above) opens the full-screen client. Screens and global keys (see docs/architecture/tui.md for the authoritative table). Plain g <key> always replaces the current screen; Ctrl+g <key> opens the same destination beside it in the second pane instead, when there's room for one. Every action key (E, l, r, …) acts on whichever pane has focus — its title is marked with a leading > (and shown bold) so the focused pane is visible even in plain mode. Tab moves focus between the two panes; Ctrl+W h / Ctrl+W l move focus directly to the primary/secondary pane (both are no-ops when the screen isn't split):

KeyAction
g hgo to Home feed (posts from people you follow)
g lgo to Local feed (public posts on this node)
g pgo to your own profile
g ngo to notifications
g bgo to your bookmarks
g dgo to direct messages
g cgo to communities on this node
g eedit your display name, bio and nameplate
g s / /search
g vgo to your own Patches Page
Ctrl+Glike g <key>, but opens the destination in the second pane instead of replacing the current screen
Tabwhen the screen is split, move focus between the two panes (the focused one is marked >)
Ctrl+W h / Ctrl+W lwhen the screen is split, move focus directly to the primary / secondary pane
: / Ctrl+Pcommand palette — every key above, by name
:privacyprivacy notice, discoverability, account export and deletion
:followrequestspending requests to follow your locked account
:filtersyour own bring-your-own filters
:listsbrowse, subscribe to, and publish filter lists
:labelerssubscribe to labelers and set per-value actions
:appealsfile and track appeals against a moderation notice
:modlogthis node's public, anonymized moderation log
ccompose a new post
j / ↓move down one post
k / ↑move up one post
n / spaceload the next page of posts
Ctrl+Rrefresh the current screen
Enteropen the selected post's thread
rreply to the selected post
llike/unlike the selected post
bbookmark/unbookmark the selected post
Rrepost/unrepost the selected post
Qquote the selected post in a new post
ddelete your own selected post (confirm y/n)
Hview the selected post's edit history
#open the selected post's first tag timeline
tsearch tags
popen the selected post's author profile
ffollow/unfollow the profile you're viewing
Jjoin/leave the community you're viewing
Bblock/unblock the profile you're viewing (confirm y/n)
Mmute/unmute the profile you're viewing (confirm y/n)
!report the selected post/profile to the moderators; anywhere else, file an issue — bug, jank, or idea
vvisit the selected actor's Patches Page (or patches visit @handle)
eedit your own Patches Page (opens $EDITOR)
oopen the selected post's first attachment externally
Llogin / switch accounts
Ctrl+Dtoggle the direct-message drawer (falls back to g d)
Ptoggle plain mode (strip nameplate decoration)
?help — the full keymap, grouped; j/k scrolls, Space/PgDn pages
qquit
Esccancel the current modal/action; back one level otherwise

This table is checked against the TUI's own binding table by apps/tui/test/docs-keymap.test.ts, so it cannot drift from the keys the app actually ships. Press ? in the app for the complete, contextual list.

Composing and attaching images ​

Press c to compose. Ctrl+S is the only way to submit — Enter always inserts a newline, so you can't accidentally post mid-thought. The body is a measured multiline editor: arrow keys and Home/End move the cursor, Alt+Left/Alt+Right jump a word, Ctrl+E jumps to end of line, Ctrl+K deletes to end of line, Ctrl+W deletes the previous word, and Ctrl+Z/Ctrl+Y undo/redo. The character counter and limit come from the node's own GetNodeInfo limits, not a hardcoded number, so it tracks whatever the server you're posting to allows.

Typing @ or # opens an autocomplete popover that looks up actors or tags as you type (debounced, so a fast typist never sees a stale result); Tab accepts the highlighted match, Esc dismisses the popover without closing compose.

Ctrl+A opens a terminal file picker to browse to a local image and attach it (uploaded, validated, and processed before the post goes out — see docs/architecture/media.md) instead of typing a raw path. Pasting a file path or file:// URI attaches it directly rather than inserting it as text. Ctrl+X removes the most recently attached image. Ctrl+T toggles a single-line content warning field. Ctrl+O swaps the editor for a live preview of the rendered post. Esc closes the compose screen without discarding your draft; drafts persist locally so a stray Esc or terminal resize won't lose your text. c opens a compact quick-post overlay sharing the same draft and editor as full compose (never a second copy of the editing logic); Ctrl+F expands it into the full compose screen (C also opens full compose directly) without losing what you've typed. Pressing E on your own post reopens compose in edit mode, prefilled with the existing body.

g s or / opens search. Tab (or 1/2/3 while the query field is empty) switches between three modes: people (handle prefix and display-name match, or a remote user@domain lookup if you're signed in), posts, and tags. In posts mode, three tokens inside the query are parsed out before the search runs: since:YYYY-MM-DD, from:@handle (or from:handle), and #tag — from: reaches the server as a real filter, since: and #tag are applied to the results locally (the screen says "filtered locally" when that happens, since a local filter can only narrow what the server already sent back). ↑/↓ recall your last 20 searches, persisted across restarts. There is no way to sort or rank search results — posts always come back newest-first. From a profile, f follows or unfollows; Ctrl+A/attach and the rest of compose behave the same whether you're posting fresh or replying (r).

Followers and following lists. From any profile in the TUI, press F to open that actor's Followers screen or G to open their Following screen (signed-in users can also run :followers or :following in the command palette to open their own). j/k move through the actor list, Enter or o opens the selected actor's profile, and Esc returns. On the web client (/@handle), click the Followers or Following tabs (or their count pills in the header) to browse followers and following.

Notifications ​

g n lists notifications (replies, mentions, likes, follows), deduplicated server-side.

Direct messages ​

Both the terminal client and the web client hold a crypto runtime and can start, read, and send into a conversation (ADR 0033 unified the identity transcripts and ADR 0035 made conversation creation a reserve). Every conversation is end-to-end encrypted (E2EE_V1); the plaintext, server-readable mode this client used to support is retired and can no longer exist. In the terminal, Ctrl+D toggles a direct-message drawer beside the timeline on wide terminals (the dedicated full-screen g d isn't wired into the shell yet); on the web, /messages and /messages/:id are the list and thread routes. Starting a new conversation is a handle/display-name search — never a raw id — that surfaces the people you already follow first when the query is empty and falls back to node-wide search once you type; a conversation still requires a mutual follow, so there is no message-request flow to fall back on. The first line is always the same disclosure — "End-to-end encrypted. This node cannot read these messages, but it can see who you message and when." — because that's true of every conversation; the second clause is the honest part of the claim and is never dropped. Nothing in this client ever calls a DM "encrypted," "secure," or "private" outside that fixed sentence. Sending is optimistic: your message appears immediately, marked as sending; if it fails to actually send, the draft comes back into the compose field instead of silently vanishing, so you can just try again. Node policy honestly reports 0 (indefinite retention, B-061); any future automatic message deletion will be displayed on this screen when configured.

Production E2EE is not yet a reviewed capability (independent review pending, ADR 0020 §12, P13-014) — the default node keeps DMs switched off entirely (there is no plaintext fallback to drop back to). An owner-authorized disposable node may explicitly advertise ADR 0027's isolated-test mode, regardless of its runtime NODE_ENV; any client surface that lets you create or read one of those conversations must persistently show “Unreviewed development E2EE — for testing only; do not use for sensitive conversations.” Treat its data as disposable. That warning is not an external-review or security claim, and it does not replace the conversation's routing-metadata disclosure.

Device linking and identity recovery ​

Because DMs are end-to-end encrypted, a second device needs to either be linked by a device that already holds the account's messaging identity, or — if no such device is reachable — used to mint a brand-new identity. Both are headless CLI commands (ADR 0037), so they work over plain stdout with no images or Kitty dependency:

bash
patches e2ee link                    # run on the NEW device
patches e2ee approve-link [<link-id>]  # run on a device that already holds the identity
patches e2ee rotate-root             # last resort: no device holds the identity anymore
patches e2ee export-recovery [--out <path>]
patches e2ee import-recovery <path>
  • patches e2ee link starts a link offer for the current device and prints a five-group, four-digit short authentication string (SAS), e.g. 0412-3399-0007-4021-1888. Compare it against the same code shown by patches e2ee approve-link on a device that already has the account's messaging identity, then approve it there. This command polls until the offer is approved, expires (10 minutes), or you press Ctrl-C.
  • patches e2ee approve-link [<link-id>] lists this account's pending link requests (or just one, if a link id is given), shows each one's SAS, and asks Does the code on the other device match? [y/N] before approving — a mismatch is discarded, never retried silently. Only a device that holds the account's messaging-root key (the authority device) can run this; running it non-interactively without a terminal refuses rather than guessing.
  • patches e2ee rotate-root is the recovery path for when no device holds the account's messaging identity anymore. It mints a brand-new identity generation after an explicit y confirmation — every contact you message afterward sees a hard identity-change warning, and message history on any device other than this one is not recoverable, so this is not something to run casually.
  • patches e2ee export-recovery [--out <path>] (default ./patches-recovery-archive.pvearc) seals this device's messaging-root key and current device roster into a recovery archive under a freshly generated recovery code, printed exactly once — write down the code and store the archive file somewhere separate from it. patches e2ee import-recovery <path> opens that archive, prompts for the recovery code (not echoed), and prepares this device to become a messaging authority again; it does not finish enrollment by itself, so open the TUI's Accounts → Devices screen afterward to enroll the device.

None of these commands ever print a private key, offer, or signature — only the SAS, device ids, and fixed status copy.

Blocking, muting, and reporting ​

From a profile: B blocks (removes any existing follow in either direction), M mutes (doesn't touch follows), both idempotent and confirmed with y/n. ! reports the selected post or the profile you're viewing, with a reason and optional free text. patches-admin (the moderator-facing CLI, not covered here — see docs/operations/moderation.md) is how a node's administrators review reports, suspend accounts, and act on them.

Reporting the app itself is always available, not just when something breaks: ! opens the beta issue reporter from any screen (:report from the palette does the same) — a bug, something janky, or a feature idea all belong there. It sends a redacted diagnostics bundle (app version, node address, recent errors as status codes only, navigation trail, last screen text — message contents are never included) and works signed out. On the web the "Report an issue" chip sits on every page; if a render error takes a route down, its error screen opens the report form directly.

Patches Pages ​

patches visit @handle[/slug] (or v from a profile/post) opens straight to that actor's Patches Page — a personal, declarative profile page (text, markdown, links, your recent posts, image galleries, a "Top 8"-style friend list, a mutual-follows "Friends" list, and a guestbook other users can sign). It is inert data, never executable code, in every client. Pinned posts (if the owner has any) show above the page's own content. [/] switch between sub-pages, j/k move between the links on the current one and Enter opens the selected one in your browser.

The block layout is responsive: narrow terminals get a single column in document order; standard-width terminals split the page-owner's prose/media into a main column with "Top 8"/badges/friends/links in a right-hand rail; wide terminals split that rail further into two columns. A page's own theme (accent colour, border) never leaks into the shell's own chrome — it only ever colours the page's own box, and plain mode (P/PATCHES_PLAIN) strips it entirely regardless of what the page author set. ASCII-art blocks are centred and clipped (never wrapped) to whatever width they're rendering at.

Press e on your own Page to edit the raw document in your $EDITOR (whatever $EDITOR is set to in your shell); save and exit to publish the new revision. E opens a structured, block-by-block editor instead: j/k select a block, J/K reorder it, a adds a new one from a type picker, d deletes it (y/n confirms), Enter edits the selected block's own fields in a small form (Tab/arrows move between fields, ←/→ cycle an enum field), Ctrl+S on a field form commits it back to the block list, and Ctrl+S on the block list validates and saves the whole document via the same UpdatePage call the $EDITOR flow uses. Esc at any point backs out one level, keeping whatever you'd typed as a local draft — nothing is lost by backing out of either editor. On the web client (/@handle), profile owners can switch to the Wall tab and click + Edit Wall to open EditWallDialog, allowing you to add and remove blocks on the wall (the index sub-page) and save the result in the browser — there's no in-dialog reordering or in-place block editing yet (delete and re-add instead). Any other sub-pages and the document's theme, created via the TUI's structured or $EDITOR editor, are preserved untouched.

Privacy, filters, filter lists, and labelers ​

:privacy shows this node's privacy notice (with the version you last acknowledged), your discoverability preferences (j/k to move, l/Space to toggle and save one at a time), account export status, and account deletion — d requests deletion after this node's grace period, u cancels a pending one while still inside it. It's also reachable from , (Preferences) as its own row. The headless equivalent is patches privacy show|set|ack|export|delete|cancel-delete.

:filters lists your own bring-your-own filters (spec §198) — literal substring/word/tag/ actor/domain matches you author yourself, never a regular expression, applied only to your own timelines. n opens an inline form (name, term kind, term value, action); X deletes with a confirm. Multi-term filters and JSON import/export are CLI-only: patches filter list|create|delete|export|import.

:lists browses filter lists other people or communities have published (Tab switches to your own subscriptions), S subscribes (defaulting to collapse, the least destructive useful action), U unsubscribes, p publishes one of your own. Subscribing never creates a block, and unsubscribing is instant. Per-entry exceptions ("this list is right about everything except this one account") are CLI-only: patches lists browse|mine|entries|publish|subscribe| unsubscribe|exception.

:labelers lists labelers on this node, S/U subscribe/unsubscribe, h/l pick a vocabulary value and a cycles its action (ignore/warn/collapse/hide) — a value the node has marked mandatory can't be changed. A label is only ever visible to viewers who subscribed; subscribing never affects anyone else. Headless: patches labelers list|subscribe|unsubscribe| action.

:filters, :lists, and :labelers are each also reachable from , (Preferences) as their own row, the same way :privacy is.

If your account is locked, :followrequests lists pending requests to follow you; A accepts, D declines.

Appeals and the moderation log ​

If you're warned, suspended, or otherwise acted on, you get a moderation notice — :appeals lists your notices (Tab switches to appeals you've already filed) and n files one against the selected not-yet-appealed notice, with a short statement. Headless: patches appeal list|create|show.

:modlog is this node's public, anonymized moderation log — a transparency record of the node's own conduct, not of any individual's. Domain entries name the domain; account/post/ media entries never carry a handle, actor id, or post id. No sign-in required. Headless: patches modlog.

Themes and colour ​

, opens Preferences. The Theme row previews live as you cycle it (h/l or arrow keys) — the whole UI repaints in the theme under the cursor before you commit to anything — and shows a line explaining its contrast against the background (e.g. "AA contrast 7.12:1 against background"), the same WCAG AA floor (4.5:1 for normal text) the nameplate colour picker enforces. Enter saves the previewed theme (and the rest of the row's settings) to this node+account's local preferences; Esc reverts everything back to what you had before you opened the screen. A custom nameplate colour (g e to edit your profile) goes through the same picker and the same floor — it degrades the swatch preview itself through truecolor → 256-colour → 16-colour → text depending on what your terminal reports, so what you see while picking is what you'll actually get.

Six themes ship: patches, paper, mono (no colour at all — bold/dim/inverse only), hacker, pastel, and terminal (never paints a background, even in a colour-capable terminal — every colour comes from your own terminal palette). Select one with --theme <name>, the PATCHES_THEME environment variable, or the Preferences row above; precedence is --theme > PATCHES_THEME > your saved preference > patches.

You can also author your own theme as JSON at $XDG_CONFIG_HOME/patches/themes/<name>.json (~/.config/patches/themes/<name>.json if XDG_CONFIG_HOME isn't set), then select it the same way (--theme <name>/PATCHES_THEME=<name>/the Theme row). Every one of the 13 colours the app uses is required in the file (a value can be a "#rrggbb" hex string or null to delegate that colour to your terminal) — an invalid or incomplete file never crashes the app, it falls back to patches and shows a one-line explanation. See apps/tui/src/theme/README.md for the exact field list and a worked example.

Regardless of which theme is active, colour always degrades to what your terminal actually supports — truecolor down to 256-colour, 16-colour, or no colour at all — and Patches honours NO_COLOR and TERM=dumb automatically. You never need to pick a "low-colour theme" separately; every theme works everywhere, just with less colour precision on a less capable terminal.

Glyphs. The Glyphs row on the same screen cycles unicode → nerd → ascii, with a live preview of what each looks like. No control anywhere in Patches requires a glyph to function — every one has a word next to it, and ascii never uses anything outside the base ASCII range. nerd (Nerd Font icons) is opt-in only and never auto-selected. PATCHES_GLYPHS (unicode/nerd/ascii) overrides both the saved preference and the automatic locale-based default for the current run.

Configuration file ​

Preferences you save from , (theme, plain mode, quiet feed, glyph set, image policy, linear mode) are written to $XDG_CONFIG_HOME/patches/preferences.json (~/.config/patches/preferences.json by default), one entry per node+account so multiple accounts or nodes never share settings. You never need to edit this file by hand — the Preferences screen is the editor — but if you do, or if it becomes corrupted (e.g. truncated by a crash mid-write, or hand-edited incorrectly), Patches never fails to start: an unreadable or invalid file is treated as "nothing saved yet" and the app falls back to command-line flags, environment variables, and built-in defaults.

Plain mode and accessibility ​

Pass --plain (or set PATCHES_PLAIN=1), or press P at runtime, to strip nameplate decoration (colored badges/frames) from the UI — useful for screen readers, low-color terminals, or just personal preference. Plain mode always shows the plain placeholder box for images (see below) rather than any form of art.

Pass --linear (or set PATCHES_LINEAR=1), run :linear at runtime, or toggle the Linear mode row on the Preferences screen (,), for linear/screen-reader mode: one column regardless of terminal width (no split panes), no overlays or drawers (they open as a full-screen takeover instead), every list row prefixed with its 1-based position ([1], [2], …) so you can refer to "item 3" without a persistent cursor, and plain mode is always implied. Like the other Preferences rows, toggling it there previews immediately and only persists once you press Enter to save.

Image rendering: Kitty graphics, terminal art, or a plain box ​

Image rendering gracefully degrades in three tiers, never failing or dumping raw escape codes:

  1. Kitty graphics protocol (Ghostty, kitty, WezTerm, and other terminals that implement it) — the real image, transmitted out-of-band and drawn inline.
  2. Terminal art — on any other terminal, Patches draws the image itself using Unicode half-block characters (two pixels per cell, in truecolor or 256-colour depending on what your terminal reports) or, on a terminal with no usable colour at all (NO_COLOR set, TERM=dumb, or no TERM), a colourless dithered ASCII-art rendering. This is real, recognizable art, not a Kitty-only feature with everyone else stuck on a box.
  3. A plain description box (dimensions, format, "press o to open externally") — used in plain mode, when you've explicitly asked for it (below), or when nothing else applies.

The Images row on the Preferences screen cycles auto → pixel → ascii → box → off (h/l or arrow keys), with a live one-line description of what each mode does: auto picks the best of the three tiers above automatically; pixel and ascii force terminal art even on a Kitty-capable terminal; box always shows the plain box (still fetching the image, just never drawing it); off never fetches or draws anything — the box still renders from the post's own metadata (dimensions, alt text), since that's content, not decoration. The same modes are available as a one-time override via the PATCHES_IMAGES environment variable (auto/kitty/pixel/ascii/box/off) if you'd rather not touch Preferences.

Using the Web client ​

Patches also provides a responsive web GUI at https://patches-web.pages.dev (apps/web), communicating with the node over Connect/HTTP. It shares the same chronological timeline, honest DM disclosure, and no-algorithm product rules as the TUI:

  • Timelines & Threads: Strictly chronological Home, Local, Tag (/t/:tag), and Community (/c/:id) feeds. Clicking any post card opens its thread (/p/:id), which includes an inline reply composer when signed in.
  • Profiles & Walls: Profile view (/@handle) includes dedicated Followers and Following tabs with count pills, plus a Wall tab with an interactive + Edit Wall dialog for profile owners.
  • Appearance & Themes: /settings/appearance allows switching between light, dark, system-following, and named themes with live swatch preview cards.
  • Mobile & PWA: Mobile web safe-area insets (env(safe-area-inset-*)) and progressive web app (PWA) installation support are built in.
  • Switch accounts: The account menu (avatar in the header) remembers every account you've signed into locally for this node, lists them under Switch account, and lets you jump between them without re-entering credentials — or pick Add account to sign into another. Accounts are stored per node, matching the TUI's per-node credential isolation; removing a saved account discards its locally stored tokens.

Troubleshooting ​

  • Can't reach the node / connection refused. Confirm --server/PATCHES_SERVER points at the right host:port, and that the server is actually reachable from your machine (for a local dev server, confirm it's running — mise run server in another terminal, or mise run tui which builds+runs the client but does not start the server for you).
  • TLS errors against a local server. Local dev servers usually don't have a real TLS certificate — pass --insecure (or PATCHES_INSECURE=1). Don't pass --insecure against a real production node; it's a plaintext connection.
  • "no OS keyring is available" warning. Patches prefers storing your refresh token in the OS keyring (via @napi-rs/keyring); on headless environments or ones without a working keyring backend (e.g. some containers/CI, some Linux setups without a D-Bus session), it falls back to a plaintext file (mode 0600) and warns once. To silence the warning and explicitly accept the plaintext fallback, set PATCHES_ALLOW_INSECURE_CREDENTIAL_FILE=1. Without that variable set, Patches refuses to fall back silently and raises instead.
  • Change your password. While signed in, use patches keys password in the TUI, or open Settings → Sign-in methods → Change password on the web. Entering the wrong current password fails without changing the account; a successful change signs out other live sessions.
  • Lost/forgot your password. The web login screen offers the recovery-email RequestPasswordReset/ResetPassword flow. The TUI exposes the authenticated change path (patches keys password); if you cannot sign in and have no recovery email, use another credential such as an SSH key or recovery code.

Reporting bugs ​

This is an open-source project; file issues against the repository the patches client was cloned from. Include your client version (patches --version), the node you were connecting to (not the account/credentials), and the exact command or in-app action that triggered the problem.

Released under the MIT License. Built from 1f3645f.