Mobile app
The mobile app is capture-first: a floating “+” button on every screen opens a quick-capture sheet before anything else, because the phone’s job is to get a thought into your graph in under five seconds. From there it grows into a full companion: browse and search the whole knowledge base, ask an agent a question against it, work your task queue, and render any full page through a web view. It talks to the same server as every other client, over tRPC, authenticated with a paired device token.
Screens
Section titled “Screens”The app is bottom tabs plus a page stack.
The foreground push-notification feed: a severity dot, red #B4453C for high/ultra,
amber #B07C22 for medium, purple #7B5BC4 for low, grey #C9C7C0 for silent, next to
a title and sub-line, newest first. Tapping a notification deep-links into the relevant
screen or page (see Push, below). This mirrors the severity model from
notifications.
Browse
Section titled “Browse”A first-class browse/search surface over the whole graph, not a cut-down mobile search
box. The search field runs kb.search, the same hybrid search
cmd-K uses on the web, capped at 20 results and debounced 250ms; any hit opens as a full
page in the page screen’s web view.
With no query, Browse shows Pinned and Recent lists instead, an on-device list
(AsyncStorage) of pages you’ve opened from Browse or Ask, newest first. A star toggles a
pin; pinned pages sort first and never age out, recents cap at the last 20. Deep link:
subspace://browse.
Ask the graph a question through an agent. The prompt invokes the
default agent exactly the way ⌘↵ does on the web (agents.invoke {instruction},
the same run and approval plumbing, the same ops.agent_runs / ops.run_steps rows),
then the screen polls agents.runTree every two seconds so the answer streams in while
the run is still live rather than waiting for it to finish.
The answer shown is the run’s narrative output. Sources chips are built from two
things: any [[page]] links inside the answer text, and the pages the run’s kb.read and
kb.edit calls actually touched, including child runs. Tapping a source resolves it (via
kb.page.resolve, falling back to kb.search) and opens it in the page screen.
Consequential actions stay gated exactly like everywhere else: a pending approval renders
as an amber card with Approve & run / Deny (agents.decide), while the rest of
the run tree is a read-only, indented step list behind a steps · N toggle. See
tools & approvals for what triggers a gate in the first
place. Deep link: subspace://ask.
Task queue
Section titled “Task queue”Sections for needs-confirm, up-next, scheduled, and done, sourced from
ops.tasks.view. If your queue is locked by a not-now
focus rule, the screen shows the lock reason and a reopen countdown instead of cards. Rows
resolve through ops.tasks.resolve: Confirm or Dismiss on needs-confirm cards, Done on
up-next cards.
Review
Section titled “Review”Flashcard review surfaces through the task queue today: due spaced-repetition cards for you appear there like any other card, resolved the same Confirm/Dismiss/Done way. A dedicated flip-and-grade review screen is where native FSRS grading will live once it does; until then, the task queue is where review happens from the phone.
Settings
Section titled “Settings”The serverUrl this install talks to (persisted in AsyncStorage, http://127.0.0.1:4780
by default in development, a Tailscale address on a real phone), the pending-capture
count with a manual “Flush now” button, and the device’s pairing token.
Quick capture
Section titled “Quick capture”A text field with a route: chip row underneath: inbox by default, plus any #tags
already typed, recently-used tags, and a live page search. Typing a #page fragment
queries kb.search and tapping a result inserts that page as a tag chip.
Routing itself is server-side: the first #tag in the capture text that resolves to a
page names the target inbox via ops.inbox.capture (the same inbox entry
point desktop and web use), so the chip row is a hint, not the router.
The mic button (expo-av) records audio, uploads it to POST <serverUrl>/files
(multipart, field file), and files the capture as [voice note](file:<id>).
Transcription happens server-side after the upload: the app’s job is just to get the audio
link filed quickly, the transcript fills in on the page afterward.
Offline capture queue
Section titled “Offline capture queue”Every capture is appended to an on-device FIFO (subspace.captureQueue.v1) before it’s
sent anywhere, so a capture is never lost to a bad connection. A flush loop drains that
queue on capture, on app foreground, and every 30 seconds, always in ts order, and it
stops at the first failure: a thrown send (airplane mode, no signal) halts the drain
rather than skipping ahead and reordering captures, and already-sent ids are skipped on
retry so a partial flush never double-files. The “+” button carries a badge with the
pending count so you can see queued captures at a glance.
Push notifications
Section titled “Push notifications”Built on expo-notifications: the app requests permission, registers an Expo push token
through ops.push.register (stored on the device row), and the server’s push provider
delivers the same severity-templated payloads used everywhere else,
marked time-sensitive for ultra severity so it can break through Focus modes on iOS.
Tapping a notification maps its payload (data.{route, pageSlug}) to a deep link:
subspace://tasks for queue and ultra-severity routes, subspace://p/<slug> when a page
slug is present, otherwise subspace://today.
Native share sheet
Section titled “Native share sheet”A real iOS share-extension target, not a workaround, built by the expo-share-extension
config plugin (Expo doesn’t ship one out of the box). Because a share extension runs in
its own process with no Tailscale reachability and no app session, it can’t call the
server directly: it appends {text?, url?, ts} as a JSON line to an App Group container
file. The main app drains that file on its next foreground, filing each line through
ops.inbox.capture and then deleting it, so sharing a link or a snippet from any other
iOS app queues exactly like an in-app capture.
Full pages via web view
Section titled “Full pages via web view”Rebuilding the outliner in React Native isn’t worth the
duplication, so a full knowledge base page renders in a react-native-webview pointed at
<serverUrl>/p/<slug>, the same SPA every other client runs, inheriting its editing,
elements, and rich-text behavior unchanged.
Server surface used
Section titled “Server surface used”The app is deliberately self-contained: a hand-rolled tRPC 11 client (no transformer)
against <serverUrl>/trpc, carrying x-subspace-device-token on every request.
| Procedure / route | Used by |
|---|---|
ops.inbox.capture |
Quick capture, offline flush, share-sheet drain |
ops.tasks.view / ops.tasks.resolve |
Task queue |
ops.push.register |
Push registration |
kb.search |
Browse, #page routing in capture |
kb.page.resolve |
Resolving an Ask source citation |
agents.invoke / agents.runTree / agents.decide |
Ask |
POST <serverUrl>/files |
Voice-note upload |
<serverUrl>/p/<slug> |
Full page rendering |