Memory.Wiki iOS — Spec
Status (2026-05-28): W9 scaffold + brand parity + Supabase auth + Apple/Google/GitHub/Email providers shipped. Real OAuth wire-up live (Supabase dashboard configured). Share Extension, Camera, Widget, Spotlight, background sync still queued for W9–W12.
Repo path:
apps/ios-native/· Bundle ID:wiki.memory.MemoryWiki· URL scheme:memorywiki://· Deployment target: iOS 17
1. North star
iOS native companion to memory.wiki. The web is the canonical surface for the URL architecture (/<id> doc, /b/<id> bundle, /@<slug> profile); iOS gives the user a fast capture path from any other app on their phone and a polished reader for their own timeline. The product story stays the same: one URL every AI can read.
The iOS app must read as a continuation of memory.wiki — same fonts, same colors, same micro-affordances. Not a "mobile version of a web app." A native surface that happens to share the brand exactly.
2. Scope
In (W9–W12)
| Surface | Status |
|---|---|
| App shell (Timeline / Capture / Profile) | done 1adec1e5 |
| Brand system (Cal Sans, JetBrains Mono, Noto Sans, dark zinc, lime micro-accent) | done |
| Auth — Apple, Google, GitHub, Email via Supabase SDK | done 2386944e |
| Animated brand blob (WKWebView, same SVG as web) | done 1adec1e5 |
| Share Extension (any app → Memory.Wiki) | queued |
| Camera + screenshot capture (OCR) | queued |
| Widget (home + lock screen) | queued |
| Spotlight indexing | queued |
| Background sync | queued |
| Push notifications | queued |
Out (post-v8 / never)
- Voice memo capture (low Korean cultural fit, scope creep)
- Live Activities, Action Extension, custom keyboard (v8.1 at earliest)
- Built-in chat with an AI (conflicts with cross-AI thesis)
- Mac Catalyst (use the web)
3. URL architecture (shared with web)
The iOS app uses the existing API surface. No iOS-specific endpoints.
| Memory.Wiki URL | iOS open behaviour |
|---|---|
https://memory.wiki/<id> |
viewer (in-app) for own docs, ShareLink for outbound |
https://memory.wiki/@<slug> |
the "use" surface — shown on Profile tab with Copy for AI |
memorywiki://auth-callback |
OAuth redirect handled by AuthManager.handleCallback |
memorywiki://... (future) |
Universal Link target after apple-app-site-association lands |
4. Brand system (parity)
| Token | Value | Notes |
|---|---|---|
Brand.background |
#09090B (zinc-950) |
App canvas. Forced via UIUserInterfaceStyle: Dark. |
Brand.surface |
#18181B |
Card / row fill. |
Brand.borderDim |
rgba(39,39,42,0.6) |
Hairline. |
Brand.textPrimary |
#FAFAFA |
Body + headings. |
Brand.textFaint |
#8A8A91 |
Captions, mono labels. |
Brand.accent |
#B5FF1A (lime) |
Micro-color only — dots, badges. Never as button fill / chip fill / body text per the v8 color-balance rule. |
Brand.display(size:) |
Cal Sans Regular | Display headings + the wordmark. |
Brand.body(size:weight:) |
Noto Sans 400/500/600 | All prose. |
Brand.mono(size:weight:) |
JetBrains Mono 400/500 | Captions, URLs, numeric data. |
Logo
MemoryWikiLogo ports apps/web/src/components/MemoryWikiLogo.tsx:
- Inline lockup: mark left +
memory.wiki(lowercase) wordmark right - Mark scales
size × 1.55≤30pt,size × 1.25>30pt - Mark =
AnimatedBlob: bundledmwblob_morph.svg(web's morph SVG) inside a transparentWKWebViewso the 10.867s breathing animation lives — asset catalog SVG renderer strips<animate>
Rule list (locked from memory.wiki/SKaY7VJP)
- No em-dash / middle-dot / emoji in UI strings
- Quiet pickers:
--surfacelift + ink check, no colored fills - Selected state never uses lime border
- "Sign in" buttons are ink, not lime (color-balance)
5. Project layout
apps/ios-native/
├── project.yml # XcodeGen spec, single source of truth
├── Config/
│ ├── Secrets.xcconfig.example # checked in
│ └── Secrets.xcconfig # gitignored — Supabase URL + anon key
├── MemoryWiki/
│ ├── MemoryWikiApp.swift # @main, onOpenURL → AuthManager
│ ├── BrandTheme.swift # color tokens + font helpers + QuietButtonStyle
│ ├── MemoryWiki.entitlements # com.apple.developer.applesignin
│ ├── Info.plist # xcodegen-rewritten; do not hand-edit
│ ├── Assets.xcassets/ # brand SVGs (logo-full, icon-app, mwblob static fallback) + Providers/{google,github}
│ ├── Fonts/ # CalSans, JetBrainsMono, NotoSans TTFs
│ ├── Resources/ # mwblob_morph.svg (animated, WKWebView consumer)
│ ├── Models/Document.swift
│ ├── Networking/
│ │ ├── SupabaseConfig.swift # shared SupabaseClient from xcconfig
│ │ ├── AuthManager.swift # @MainActor session + 4-provider sign-in
│ │ └── APIClient.swift # REST wrappers around /api/user/documents + /api/docs
│ └── Views/
│ ├── RootView.swift # custom TabBar (Timeline / Capture / Profile)
│ ├── AuthView.swift # 4-provider sign-in with brand surfaces
│ ├── EmailAuthSheet.swift # half-sheet, sign in / create-account toggle
│ ├── TimelineView.swift # bordered card list of /api/user/documents
│ ├── CaptureView.swift # fast textarea → POST /api/docs
│ ├── ProfileView.swift # the use surface: hub URL + Copy for AI
│ ├── MemoryWikiLogo.swift # canonical inline mark + wordmark
│ └── AnimatedBlob.swift # WKWebView host for the morph SVG
├── scripts/
│ └── generate-apple-client-secret.sh # ES256 JWT generator (Apple sign-in secret)
└── README.md
6. Auth model
Supabase-iOS SDK is the canonical client. The SDK persists the session in the Keychain automatically; AuthManager is a thin @MainActor ObservableObject over it that publishes a UserSession value type for SwiftUI to drive UI off.
| Provider | Flow | Notes |
|---|---|---|
| Apple | ASAuthorizationController → signInWithIdToken(.apple, idToken:nonce:) |
Required by App Store guideline 4.8 when offering social SSO. Nonce: random URL-safe string + SHA-256 sent in request.nonce. |
signInWithOAuth(.google) → ASWebAuthenticationSession |
Real multi-color G logo, not SF Symbol. | |
| GitHub | signInWithOAuth(.github) → ASWebAuthenticationSession |
Real Octocat. |
signIn(email:password:) / signUp(...) |
Native form in EmailAuthSheet. |
Apple client secret rotation
Apple caps the client-secret JWT lifetime at ~6 months. .p8 private key never expires; the JWT signed with it does.
scripts/generate-apple-client-secret.sh shells out to openssl + python3 to produce the ES256 JWT (Apple's spec — ASN.1 DER signature unpacked to raw R‖S):
bash./scripts/generate-apple-client-secret.sh \
--p8 ~/Downloads/AuthKey_XXXXXXXXXX.p8 \
--team-id XXXXXXXXXX \
--key-id XXXXXXXXXX \
--client-id wiki.memory.MemoryWiki.signinservice
Paste output into Supabase dashboard → Authentication → Providers → Apple → Secret Key (for OAuth). Re-run 1 week before expiry. Current expiry: 2026-11-25.
Crash already fixed
ASWebAuthenticationPresentationContextProviding.presentationAnchor is nonisolated per protocol; the early implementation called DispatchQueue.main.sync from the main thread inside that method, which is an instant deadlock. Replaced with MainActor.assumeIsolated (the correct pattern — ASKit guarantees the call is on main, we just need to read window state without dispatching).
7. Networking
APIClient is a hand-written URLSession wrapper around the existing web API. Identity headers (Authorization: Bearer <session.accessToken> + x-user-id + x-user-email) are pulled from AuthManager.shared.session on every call so a sign-in elsewhere in the app picks up without recreating the client.
Endpoints in use today:
GET /api/user/documents— Timeline listPOST /api/docs— Capture write (source:ios)
Endpoints used implicitly via Supabase SDK:
/auth/v1/*— sign in / sign up / OAuthprofilestable SELECT forhub_sluglookup (RLS gated)
The doc / bundle / @username URLs are NOT fetched as JSON — they're shared via ShareLink (URL out) or rendered inside an in-app browser later. Dual-response on the server side means any AI client paste-fetching them gets clean markdown automatically.
8. Build & run
bash# one-time setup
brew install xcodegen
# regenerate the project file (after editing project.yml or
# adding / removing source files)
cd apps/ios-native
xcodegen generate
# resolve SPM packages once
xcodebuild -resolvePackageDependencies \
-project MemoryWiki.xcodeproj -scheme MemoryWiki
# build for the simulator
xcodebuild -project MemoryWiki.xcodeproj -scheme MemoryWiki \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-configuration Debug -derivedDataPath build build
# open in Xcode (recommended day-to-day)
open MemoryWiki.xcodeproj
Config/Secrets.xcconfig must exist before the build will compile — copy Config/Secrets.xcconfig.example and fill in the values from apps/web/.env.local.
9. Out-of-repo prerequisites
Done (founder):
- Supabase: project URL + anon key in
Config/Secrets.xcconfig;memorywiki://auth-callbackallow-listed in Auth → URL Configuration → Redirect URLs; Apple / Google / GitHub providers turned on - Apple Developer: App ID
wiki.memory.MemoryWikiwith Sign in with Apple capability; Service IDwiki.memory.MemoryWiki.signinservicebound to the Supabase callback;.p8key generated, stored in~/Downloads/(gitignored, not in repo); Supabase Apple-provider JWT generated (expires 2026-11-25) - Xcode: Team selected under Signing & Capabilities; Sign in with Apple capability added (writes the entitlement file the spec already commits)
Pending:
- Web
/auth: accept?provider=google|github|emailso iOS'ssignInWithOAuth(provider:)jumps straight into the provider's flow without bouncing through the chooser - Apple Universal Links: ship
apple-app-site-associationonmemory.wiki/.well-known/once the team ID is finalised sohttps://memory.wiki/<id>opens the iOS app on tap - TestFlight: first build push for on-device validation of the Apple flow (simulator can render the button but can't complete the iCloud sign-in)
10. Current verification
xcodebuild -destination 'iOS Simulator' build → BUILD SUCCEEDED
Fresh-install launch via xcrun simctl install / launch:
- Dark zinc canvas paints at first frame (no white flash)
- Brand lockup renders with the morph blob actually breathing (verified by 3 sequential screenshots showing shape change)
- 4 provider buttons render with real logos (Apple system button, multi-color Google G, Octocat, envelope)
- Google button taps now open
ASWebAuthenticationSessioncleanly (previous nonisolated deadlock fixed)
11. What's next
Next 3 commits (in order):
- Web
/auth?provider=— accept the param + redirect into the requested provider directly. Unblocks Google / GitHub real round-trip. - Share Extension target —
ShareViewControllerwith App Group glue, posts the captured URL/text to a tiny on-device queue the main app drains on launch. Lets the user grab anything from anywhere →memory.wiki/<id>in two taps. - Widget (home screen, small) — last 3 captures + a "+ Capture" button that deep-links into the Capture tab.
After that: camera + OCR, Spotlight, background sync, push, then channel updates (Chrome / VSCode / Desktop / CLI / MCP) align with the iOS scope.
12. Linked decisions
- Founder pushback on iOS scaffold being "generic SwiftUI shell" → full brand rewrite (this spec)
- Comments feature dropped on web → not in iOS scope either
- Type 3 synthesis docs dropped → paste-anywhere via
ProfileViewcovers the iOS use surface /hub/<slug>→/@<slug>301 on web → iOS Profile shows/@<slug>directly
Plan parent: memory.wiki/SKaY7VJP