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: bundled mwblob_morph.svg (web's morph SVG) inside a transparent WKWebView so 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: --surface lift + 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 ASAuthorizationControllersignInWithIdToken(.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.
Google signInWithOAuth(.google)ASWebAuthenticationSession Real multi-color G logo, not SF Symbol.
GitHub signInWithOAuth(.github)ASWebAuthenticationSession Real Octocat.
Email 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 list
  • POST /api/docs — Capture write (source: ios)

Endpoints used implicitly via Supabase SDK:

  • /auth/v1/* — sign in / sign up / OAuth
  • profiles table SELECT for hub_slug lookup (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-callback allow-listed in Auth → URL Configuration → Redirect URLs; Apple / Google / GitHub providers turned on
  • Apple Developer: App ID wiki.memory.MemoryWiki with Sign in with Apple capability; Service ID wiki.memory.MemoryWiki.signinservice bound to the Supabase callback; .p8 key 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|email so iOS's signInWithOAuth(provider:) jumps straight into the provider's flow without bouncing through the chooser
  • Apple Universal Links: ship apple-app-site-association on memory.wiki/.well-known/ once the team ID is finalised so https://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' buildBUILD 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 ASWebAuthenticationSession cleanly (previous nonisolated deadlock fixed)

11. What's next

Next 3 commits (in order):

  1. Web /auth?provider= — accept the param + redirect into the requested provider directly. Unblocks Google / GitHub real round-trip.
  2. Share Extension targetShareViewController with 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.
  3. 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 ProfileView covers the iOS use surface
  • /hub/<slug>/@<slug> 301 on web → iOS Profile shows /@<slug> directly

Plan parent: memory.wiki/SKaY7VJP