# Makima iOS — v1 Plan
**Date:** 2026-04-24
**Target:** iOS 18.0+ · iPhone only · Pure native SwiftUI · No Android
**Upstream:** `soryu/makima` (Rust + Postgres server, React frontend at `makima.jp`)
**Location:** new folder `apps/ios/` inside the existing `soryu-co/soryu` monorepo (sibling to `frontend/`, `makima/`, `k8s/`).
**License:** MIT (carry repo-level LICENSE; no per-folder LICENSE needed).
**App Store seller:** Soryu LTD.
**Bundle ID:** `co.soryu.makima` (dropped the `.ios` suffix per preference).
---
## 1. Product shape
**Primary surface: Composite Home.** A glanceable dashboard with section cards:
- **Active Contracts** — count + top 3 running, tap → Contracts list
- **Active Daemons** — mesh status pill (N online / M offline)
- **Pending Directive Questions** — amber badge, tap → Directives
- **Latest Listen Session** — last transcript preview, tap → Listen detail
- **Recent Tasks** — 3 most recent tasks across contracts, tap → Task detail (live output)
Secondary surfaces reachable from home: Contracts list, Tasks list, Directives, Daemons, Listen (read-only history in v1), Settings.
**Not in v1:** Speak (TTS), Files editor, Orders write-path, mesh merge UI, worktree diff viewer, daemon reauth flow, supervisor questions *answering* (read-only badge only — answer on web).
## 2. Stack
- **Language:** Swift 5.10+ / SwiftUI
- **Min iOS:** 18.0 (same as Litter/KittyLitter — lets us use latest SwiftUI APIs, Observation, Swift Concurrency)
- **Architecture:** MV (Model + View) with `@Observable` stores — no MVVM ceremony
- **Networking:** `URLSession` + `URLSessionWebSocketTask` (no Alamofire/Starscream)
- **Persistence:** SwiftData for cached contracts/tasks/transcripts; Keychain for API key(s); `UserDefaults` for server-profile list
- **Markdown/code:** `swift-markdown-ui` (MIT) for markdown, `Splash` or `Runestone` for syntax highlighting (evaluate at impl time — Splash is lighter, Runestone handles big files)
- **Push:** APNs via a small `services/push-proxy` later — **not in v1**. v1 uses local notifications triggered by WS events while app is foreground/recent. True background push = v1.1.
- **Build:** XcodeGen (`project.yml` source of truth, matches Litter)
- **No dependencies beyond:** swift-markdown-ui, KeychainAccess (or hand-roll), a syntax highlighter. Everything else stdlib.
## 3. Auth & server configuration
### 3.1 Multi-profile server picker *(question 3 = a: single field, but we're building the primitive that generalises)*
User picked (a) — single "Server URL" field. Implementation:
- `Settings → Server URL` text field, default `https://makima.jp`
- Stored in Keychain alongside the API key (so swapping server swaps the credential bundle)
- On change: clear cached contracts/tasks/daemons, re-fetch `/health`, prompt for new API key if 401
- Internally represented as `ServerProfile { url: URL, apiKey: String, label: String }` — list-of-one in v1, lets us upgrade to multi-profile in v1.x without migration
### 3.2 API key flow *(question 2 = c: both — but v1 = API key only)*
User said (c) both, but Supabase OAuth needs project keys and deep-link wiring. Ship API key paste in v1, add Supabase in v1.1:
1. First-launch onboarding:
- Screen 1: Server URL (prefilled `https://makima.jp`)
- Screen 2: "Open web settings → create API key → paste here" with a `Open in Safari` button to `<server>/settings`
- Screen 3: validate via `GET /api/v1/mesh/daemons` (any authenticated endpoint); on success, store and proceed to Home
2. API key stored in Keychain, scoped per-server-URL
3. `Settings → Rotate key` button — calls `POST /api/v1/auth/api-keys/refresh`, replaces stored key
### 3.3 Auth header *(verified 2026-04-24 against `src/server/auth.rs`)*
- **API keys → header `x-makima-api-key: <full-key>`** (constant `API_KEY_HEADER` line 404). Keys are prefixed `mk_`.
- **Supabase JWTs → header `Authorization: Bearer <jwt>`** (line 1039–1041).
- Order of precedence in the server: tool key → API key → JWT.
- **v1 sends `x-makima-api-key` only.** `Authorization: Bearer` is reserved for v1.1 Supabase OAuth. Don't mix.
- WebSocket: same header works via `URLSessionWebSocketTask` custom request headers (not via `?token=` query). Open Q #7 resolved — update client code accordingly.
## 4. Surfaces & endpoints
| Surface | Endpoints (v1) | Notes |
|---|---|---|
| Home | `GET /mesh/tasks?status=running`, `GET /mesh/daemons`, `GET /directives?pending=true`, `GET /listen/sessions?limit=1` | Parallel fetch, 30s refresh, pull-to-refresh |
| Contracts list | `GET /contracts` | Filter: active / completed / archived |
| Contract detail | `GET /contracts/{id}`, `GET /mesh/tasks?contract_id={id}` | Shows phase, tasks |
| Tasks list | `GET /mesh/tasks` | Filter by status, contract |
| **Task detail (hero)** | `GET /mesh/tasks/{id}`, `GET /mesh/tasks/{id}/output`, `WS /mesh/tasks/subscribe` | Live output stream, markdown+code rendered |
| Directives | `GET /directives` | Read-only; badge for pending questions |
| Daemons | `GET /mesh/daemons` | Status list, read-only |
| Listen (history) | `GET /listen/sessions`, `GET /listen/sessions/{id}` | Transcript viewer, no live mic in v1 |
| Settings | `GET /auth/api-keys`, `POST /auth/api-keys/refresh`, `DELETE /auth/api-keys` | Server URL, key rotate, about |
### 4.1 WebSocket: Task output livestream *(question 4)*
Single WS: `wss://<server>/api/v1/mesh/tasks/subscribe`
Headers on the upgrade request: `x-makima-api-key: <full-key>` (same as HTTP — verified above).
- Subscribes to all tasks the user has access to
- Server pushes task events: `task.output`, `task.status`, `task.completion_gate`, `task.supervisor_question`
- App maintains in-memory buffer per visible task, SwiftData-persists last 2000 lines on background
- Auto-reconnect with exponential backoff (1s → 30s cap), shown as pill in masthead
- When app is foregrounded, send `resync` frame with last-seen event IDs per task to catch up
## 5. Notifications *(question 5 must-have)*
**v1 (local only):**
- While app is open or recently backgrounded (iOS background WS grace ~30s), fire `UNUserNotificationCenter` local notifications for:
- Task completed
- Task failed
- Supervisor question arrived
- Notification taps deep-link to the relevant surface (`makima://task/{id}`, `makima://directive/{id}`)
**v1.1 (push):**
- Small `services/push-proxy` service (own repo or monorepo folder) that holds APNs device tokens and subscribes to the same WS on behalf of users
- Server emits "notify" events; proxy fans out to APNs
- Matches Litter's `services/push-proxy/` pattern
## 6. Aesthetic port *(question 5 must-have)*
Goal: feel like a native extension of makima.jp, not a generic iOS app.
- **Colors:** port exactly — `#0c1729` bg, `#9bc3ff` foreground, `#3f6fb3` accent, amber `#f59e0b` for alerts. Central `Palette.swift`.
- **Typography:** SF Mono for all chrome (nav, labels, ticker), SF Pro for body. Uppercase + tracked for nav labels (matches NavStrip).
- **Chrome elements to port:**
- `NAV//` prefix strip → iOS tab bar replaced with a custom masthead bar (top), tab-bar-less
- Dashed borders (`border-dashed` in Tailwind → `StrokeStyle(dash: [4, 4])` in SwiftUI)
- Masthead ticker → a thin top bar with build version + WS status + server label
- **Logo:** reuse the existing logo from the web frontend — `frontend/public/logo/makima-logo.svg` (concentric rings). Copy into `apps/ios/Sources/Makima/Resources/Logo/` at M1, render as SwiftUI `Image` with SVG support via a tiny helper (SwiftUI 18 has native SVG; fallback = rasterize to PDF asset at build time). Also port `crane-logo-transparent.png` for the Soryu masthead credit if the design calls for it.
- `JapaneseHoverText` → `JapaneseLongPressText` on iOS (long-press reveals English). Same term list: 支配する, 命令, 契約, 史料, 聴取, etc.
- **Motion:** respectful — fades and springs only. No parallax, no bounce.
## 7. Markdown & code rendering *(question 5 must-have)*
- `swift-markdown-ui` with custom theme matching web: inline code in `#1a2840` pill, code fences with language label chip, tables scroll horizontally
- Syntax highlighting via Splash (lightweight) or Runestone (full editor; overkill unless we add editing later)
- Task output stream is treated as a sequence of markdown chunks with optional `<COMPLETION_GATE>` blocks rendered as a distinct status card (not raw XML)
- Links are tappable; `file://` or repo-relative links show a "not supported on mobile" toast rather than erroring
## 8. Data model (client-side SwiftData)
```
ServerProfile { url, label, apiKeyRef, lastSeenAt }
Contract { id, name, phase, status, autonomous_loop, updated_at }
Task { id, contract_id?, parent_id?, status, plan, daemon_id?, updated_at }
TaskEvent { id, task_id, kind, payload_json, ts } // bounded by task, prune >2k
Daemon { id, status, last_heartbeat, hostname? }
Directive { id, title, status, has_pending_question, updated_at }
ListenSession { id, title?, started_at, ended_at?, transcript_preview }
```
Everything else (file contents, full transcripts, mesh details) = fetch on demand, don't cache.
## 9. Repo layout — as a folder inside `soryu-co/soryu`
```
soryu/ # existing monorepo root
apps/
ios/ # <— NEW
project.yml # XcodeGen source
Makefile # ios-device-fast, ios-sim-fast, bootstrap, lint
Sources/
Makima/
App/ # MakimaApp, entry, scene
Design/ # Palette, Typography, Components (DashedBorder, MastheadBar, JapaneseLongPressText, Logo)
Net/ # APIClient, WebSocketClient, AuthStore, ServerProfileStore
Stores/ # HomeStore, ContractsStore, TasksStore, DirectivesStore, DaemonsStore (all @Observable)
Features/
Home/ # HomeView + section cards
Contracts/
Tasks/ # TasksListView, TaskDetailView (hero), OutputStreamView
Directives/
Daemons/
Listen/ # history only in v1
Settings/
Onboarding/
Models/ # Contract, Task, TaskEvent, Daemon, Directive, ListenSession
Markdown/ # Theme, code block, completion gate block
Resources/
Logo/ # copied from ../../../frontend/public/logo/
Assets.xcassets
Tests/
MakimaTests/ # AuthStore, APIClient (URLProtocol stubs), WebSocketClient reconnect
docs/
DEVELOPMENT.md
RELEASING.md
README.md # points back to repo-root LICENSE (MIT)
frontend/ # existing
makima/ # existing
k8s/ # existing
LICENSE # repo-root MIT (confirm exists; add if missing)
README.md # add "apps/ios — Makima iOS client" line
```
## 10. Milestones
| M | Scope | Done-when |
|---|---|---|
| M0 | Repo scaffold, XcodeGen, CI (GitHub Actions `xcodebuild`), empty app launches | App opens to "Hello, Makima" on sim + device |
| M1 | Design system: Palette, Typography, DashedBorder, MastheadBar, Logo (Canvas), JapaneseLongPressText | Standalone previews render; aesthetic matches web side-by-side |
| M2 | Onboarding + ServerProfile + Keychain API key + APIClient + auth probe | Paste key, see 200 from `/mesh/daemons` |
| M3 | Home composite view (static → wired) | 5 cards render from live endpoints, pull-to-refresh |
| M4 | Contracts list + detail, Tasks list + detail (polled output first, no WS yet) | Can drill from Home → Contract → Task and see output |
| M5 | WebSocket livestream + markdown/code rendering + completion gate card | Task detail updates without polling; completion gate shows as status card |
| M6 | Directives read-only, Daemons read-only, Listen history | All nav items reachable |
| M7 | Local notifications + deep links | Foregrounded WS fires notifications that deep-link correctly |
| M8 | Polish pass + TestFlight | Review-ready build, screenshots, privacy copy |
## 11. Decisions locked (2026-04-24)
| # | Decision |
|---|---|
| License | **MIT**, carried by repo-root `LICENSE` in `soryu-co/soryu` |
| App Store seller | **Soryu LTD** |
| Bundle ID | **`co.soryu.makima`** (no `.ios` suffix) |
| Supabase OAuth | Deferred to **v1.1** |
| Logo | **Reuse** existing `frontend/public/logo/makima-logo.svg` (no redraw) |
| Privacy policy | Point to repo README (KittyLitter-style) |
| Auth header | `x-makima-api-key: <mk_…>` — **verified** against `src/server/auth.rs` |
| Push-proxy (v1.1) | `soryu-co/soryu` repo, new folder **`services/push-proxy/`** |
| Repo location | Folder **`apps/ios/`** inside existing `soryu-co/soryu` monorepo |
All blockers resolved. M0 (scaffold + XcodeGen + CI) is unblocked.
---
## Cross-checks against user preferences
- ✅ Zero-friction auth: single paste, then nothing. Supabase OAuth queued for v1.1.
- ✅ Customizable API URL: single-profile today, multi-profile-ready internally.
- ✅ Aesthetic-forward: dedicated M1 before any feature work.
- ✅ License-clean: concentric-circle logo redrawn in Canvas, no character likeness — matches "symbolic/abstract" preference. No copyrighted assets.
- ✅ Pure native: zero JS, zero RN, zero Expo, zero Rust-UniFFI.