···207207- **Phase C — Federated moderation:** Moderators on different AppView instances (or using third-party tools) can perform mod actions on the Forum DID via delegation, enabling distributed moderation teams.
208208- **Phase D — Extract and propose:** Package the delegation spec as a standalone AT Proto proposal with the forum as the reference use case. Contribute upstream.
209209210210+### Mobile Apps (iOS & Android)
211211+212212+React Native + Expo cross-platform apps consuming the same `/api/*` endpoints as the web UI. Phased rollout: read-only browse → write/interact → push notifications → offline support & app store release. Full plan in [`docs/mobile-apps-plan.md`](mobile-apps-plan.md).
213213+210214### Other Future Work
211215- Nested/threaded replies
212216- Full-text search (maybe Meilisearch)
···219223- Custom themes & branding
220224- Plugin/extension system
221225- Email notifications
226226+- Push notifications (mobile + web)
222227- RSS feeds per category/topic
+312
docs/mobile-apps-plan.md
···11+# atBB Mobile Apps — Plan
22+33+This document outlines the strategy for building mobile applications for atBB. The existing architecture — a clean JSON API (appview) separate from the server-rendered web UI — makes this straightforward.
44+55+---
66+77+## Why Mobile Apps?
88+99+The web UI (`@atbb/web`) will be responsive and mobile-friendly, but dedicated apps offer:
1010+1111+- **Push notifications** for replies, mentions, and moderation events
1212+- **Native performance** — smooth scrolling through long threads, instant navigation
1313+- **Offline reading** — cache threads and categories for subway/airplane use
1414+- **Deep OS integration** — share sheets, AT Proto URI handling, biometric auth
1515+- **Better compose experience** — native keyboard handling, image picker, draft persistence
1616+1717+The web app remains the primary interface. Mobile apps are a complement, not a replacement.
1818+1919+---
2020+2121+## Architecture Fit
2222+2323+The existing system already separates concerns in a way that supports mobile clients:
2424+2525+```
2626+┌─────────────┐
2727+│ Forum UI │──────┐
2828+│ (Web App) │ │
2929+└─────────────┘ │ ┌──────────────┐ ┌─────────────────┐
3030+ ├────▶│ AppView │────▶│ Firehose / │
3131+┌─────────────┐ │ │ (JSON API) │◀────│ User PDS nodes │
3232+│ Mobile Apps │──────┘ └──────────────┘ └─────────────────┘
3333+│ (iOS/Andrd) │
3434+└─────────────┘
3535+```
3636+3737+Mobile apps consume the **same `/api/*` endpoints** as the web UI. No new backend is needed — just the existing appview.
3838+3939+### What the appview already provides
4040+4141+| Endpoint | Purpose | Mobile use |
4242+|---|---|---|
4343+| `GET /api/forum` | Forum metadata | App title, description, branding |
4444+| `GET /api/categories` | Category list | Home screen / tab bar |
4545+| `GET /api/categories/:id/topics` | Topic list (paginated) | Category view with pull-to-refresh |
4646+| `GET /api/topics/:id` | Thread (OP + replies) | Thread view |
4747+| `POST /api/topics` | Create topic | Compose screen |
4848+| `POST /api/posts` | Create reply | Reply sheet |
4949+5050+### What needs to be added to the appview for mobile
5151+5252+| Endpoint / Feature | Purpose |
5353+|---|---|
5454+| `GET /api/users/:did` | User profile / post history |
5555+| `GET /api/notifications` | Notification feed (replies to your posts, mentions, mod actions) |
5656+| `POST /api/reactions` | Add reaction to a post (requires `reactions` table + `space.atbb.reaction` lexicon) |
5757+| `DELETE /api/reactions/:id` | Remove reaction from a post |
5858+| `POST /api/devices` | Register push notification token (APNs / FCM) |
5959+| `DELETE /api/devices/:id` | Unregister push token |
6060+| Pagination headers / cursors | Consistent cursor-based pagination across all list endpoints |
6161+| `ETag` / `Last-Modified` headers | Conditional requests for efficient caching |
6262+6363+These additions benefit the web UI too — they aren't mobile-only concerns.
6464+6565+---
6666+6767+## Technology Choice: React Native + Expo
6868+6969+**Recommendation:** React Native with Expo for cross-platform iOS and Android from a single codebase.
7070+7171+### Why React Native + Expo
7272+7373+- **Single codebase** for iOS and Android — critical for a small team / solo developer
7474+- **TypeScript** — same language as the rest of the monorepo; can share types from `@atbb/lexicon`
7575+- **Expo** simplifies builds, OTA updates, push notifications, and app store submissions
7676+- **Mature ecosystem** — navigation (React Navigation / Expo Router), state management, networking
7777+- **AT Proto libraries work** — `@atproto/api` runs in React Native with minor polyfills
7878+- **AGPL-3.0 compatible** — React Native's MIT license is compatible with the project license
7979+8080+### Why not other options
8181+8282+| Option | Reason to skip |
8383+|---|---|
8484+| Flutter | Dart — different language from the rest of the stack, can't share types |
8585+| Native (Swift/Kotlin) | Two codebases to maintain, slower iteration for a small team |
8686+| PWA only | iOS Web Push requires add-to-home-screen with constrained UX, no app store presence, weaker offline |
8787+| Capacitor/Ionic | WebView wrapper — won't feel native, performance ceiling |
8888+8989+### Monorepo integration
9090+9191+Add a new package to the existing workspace:
9292+9393+```
9494+packages/
9595+ lexicon/ # shared types (already exists)
9696+ appview/ # JSON API (already exists)
9797+ web/ # server-rendered UI (already exists)
9898+ mobile/ # NEW — React Native + Expo app
9999+```
100100+101101+The mobile package imports `@atbb/lexicon` for type safety against AT Protocol records at dev/typecheck time (ensuring the mobile app stays in sync with lexicon changes). However, the actual app builds via Expo's Metro bundler (`expo start`, `eas build`), not via `pnpm build` — Turborepo handles the `lexicon` → `appview`/`web` build chain, but mobile has its own separate build tooling.
102102+103103+---
104104+105105+## Mobile App Structure
106106+107107+### Screens
108108+109109+| Screen | Description | API |
110110+|---|---|---|
111111+| **Login** | AT Proto OAuth flow via in-app browser (exchanges tokens with user's PDS) | `@atproto/oauth-client` |
112112+| **Home** | Category list, forum branding | `GET /api/categories` |
113113+| **Category** | Topic list with pull-to-refresh, infinite scroll | `GET /api/categories/:id/topics` |
114114+| **Topic/Thread** | OP + flat replies, pagination | `GET /api/topics/:id` |
115115+| **Compose** | New topic form (select category, write post) | `POST /api/topics` |
116116+| **Reply** | Reply sheet (bottom sheet or modal) | `POST /api/posts` |
117117+| **Notifications** | Reply/mention/mod action feed | `GET /api/notifications` |
118118+| **Profile** | User info, post history | `GET /api/users/:did` |
119119+| **Settings** | Push notification prefs, theme, logout | Local + `/api/devices` |
120120+121121+### Navigation
122122+123123+```
124124+Tab Bar
125125+├── Home (categories → topics → thread)
126126+├── Notifications
127127+└── Profile / Settings
128128+```
129129+130130+Use Expo Router (file-based routing) or React Navigation with a bottom tab + stack pattern.
131131+132132+### Key Libraries
133133+134134+| Concern | Library |
135135+|---|---|
136136+| Navigation | Expo Router or React Navigation |
137137+| HTTP client | Standard `fetch` or `ky` (lightweight) |
138138+| State / cache | TanStack Query (React Query) — handles caching, pagination, background refetch |
139139+| Push notifications | `expo-notifications` + server-side APNs/FCM |
140140+| Secure storage | `expo-secure-store` (for auth tokens) |
141141+| AT Proto OAuth | `@atproto/oauth-client` (client-side OAuth + DPoP) + `expo-auth-session` (in-app browser) |
142142+| Offline storage | SQLite via `expo-sqlite` (cache threads for offline reading) |
143143+144144+---
145145+146146+## Authentication on Mobile
147147+148148+AT Proto OAuth on mobile follows the standard OAuth 2.0 + PKCE + DPoP flow for native apps:
149149+150150+1. User enters their handle or PDS URL
151151+2. App resolves the user's PDS and authorization server from the user's DID document
152152+3. App generates a DPoP key pair (stored in secure enclave/keystore) and creates a PKCE challenge
153153+4. App opens an in-app browser (ASWebAuthenticationSession on iOS, Custom Tab on Android) to the authorization URL
154154+5. User authenticates on their PDS
155155+6. PDS redirects back to the app via a custom URI scheme (`atbb://oauth/callback`) or universal link
156156+7. **App exchanges the authorization code directly with the user's PDS authorization server** (not via the appview) to obtain access/refresh tokens
157157+8. Tokens stored in `expo-secure-store` (keychain on iOS, keystore on Android)
158158+9. Subsequent API calls to the appview include a DPoP-bound bearer token
159159+10. **The appview validates tokens against the user's DID document** — it doesn't broker authentication
160160+161161+This preserves AT Proto's decentralized model: users authenticate with their own PDS, then present credentials to the appview. The mobile app needs to implement AT Proto OAuth client logic directly (the `@atproto/oauth-client` library can help, though mobile support is still maturing).
162162+163163+### DPoP Key Management on Mobile
164164+165165+AT Proto uses DPoP (Demonstrating Proof of Possession) to bind access tokens to a specific client key, preventing token theft/replay attacks. On mobile, this requires:
166166+167167+- **Secure key storage:** The DPoP private key must be stored in platform secure storage — iOS Keychain (accessed via Secure Enclave on supported devices) or Android Keystore. Use `expo-secure-store` or platform-specific crypto APIs.
168168+- **Key lifecycle:** Generate a new DPoP key pair on first login. The key should persist across app sessions but be revoked/regenerated on logout or token refresh failure.
169169+- **Proof generation:** For each API request, generate a DPoP proof (signed JWT) using the private key. The `@atproto/oauth-client` library handles this, but mobile-specific integration with secure storage may require custom bindings.
170170+171171+This is a mobile-specific concern that doesn't exist in the web UI (where DPoP keys can be ephemeral or stored in localStorage for less-critical use cases).
172172+173173+---
174174+175175+## Push Notifications
176176+177177+### Architecture
178178+179179+```
180180+User posts a reply
181181+ │
182182+ ▼
183183+ Firehose event
184184+ │
185185+ ▼
186186+ AppView indexes reply
187187+ │
188188+ ▼
189189+ Notification service checks:
190190+ "Who should be notified?"
191191+ │
192192+ ▼
193193+ Sends push via APNs (iOS)
194194+ and/or FCM (Android)
195195+ │
196196+ ▼
197197+ Mobile device shows notification
198198+```
199199+200200+### Implementation
201201+202202+- Mobile app registers its push token with `POST /api/devices` on login
203203+- Appview maintains a `devices` table: `(id, user_did, platform, push_token, created_at)` — **this is a purely local/appview-managed table** (not backed by an AT Proto record), unlike the `(did, rkey, cid, indexed_at)` pattern used for AT Proto record tables
204204+- When the indexer processes a new post that is a reply, it checks if the parent post's author or thread participants have registered devices
205205+- A lightweight push service (can be part of the appview or a separate worker) sends the notification payload
206206+- Start simple: notify on direct replies only. Expand to mentions, mod actions, thread subscriptions later
207207+208208+### Notification types (phased)
209209+210210+| Phase | Notification |
211211+|---|---|
212212+| Initial | Direct reply to your post |
213213+| Later | @mention in a post |
214214+| Later | Mod action on your post (locked, deleted) |
215215+| Later | New topic in a subscribed category |
216216+| Later | Thread subscription (get notified on any reply in a thread) |
217217+218218+---
219219+220220+## Offline Support
221221+222222+Use a layered caching strategy:
223223+224224+1. **HTTP cache** — TanStack Query caches API responses in memory with configurable stale times
225225+2. **Persistent cache** — TanStack Query's `persistQueryClient` with AsyncStorage or SQLite for across-app-restart caching
226226+3. **Explicit offline mode** — "Save thread for offline" downloads thread data to SQLite; viewable without network
227227+4. **Optimistic writes** — compose a reply offline, queue it, send when back online
228228+229229+MVP mobile app only needs layer 1 (in-memory cache). Layers 2-4 come later.
230230+231231+---
232232+233233+## Implementation Phases
234234+235235+### Mobile Phase 0: Scaffold (after web MVP Phase 4+)
236236+237237+Prerequisites: Appview API is stable with read endpoints working, AT Proto OAuth is implemented.
238238+239239+- [ ] Add `packages/mobile` with Expo + TypeScript template
240240+- [ ] Configure workspace — `@atbb/lexicon` as dependency for shared types
241241+- [ ] Set up Expo Router navigation structure (tabs + stacks)
242242+- [ ] Implement API client layer using `fetch` + TanStack Query
243243+- [ ] Create basic UI shell: tab bar, placeholder screens
244244+245245+### Mobile Phase 1: Read-Only Browse
246246+247247+- [ ] Login screen — AT Proto OAuth client (`@atproto/oauth-client` + `expo-auth-session`) with direct PDS token exchange
248248+- [ ] Home screen — category list from `GET /api/categories`
249249+- [ ] Category screen — topic list with pull-to-refresh and infinite scroll
250250+- [ ] Thread screen — OP + flat replies with pagination
251251+- [ ] Basic theming (light/dark mode following system preference)
252252+- [ ] Loading states, error states, empty states
253253+254254+### Mobile Phase 2: Write & Interact
255255+256256+- [ ] Compose screen — create new topic (select category, write text)
257257+- [ ] Reply sheet — reply to a post (bottom sheet UX)
258258+- [ ] Reactions — tap to react on posts
259259+- [ ] Profile screen — view your own posts and membership info
260260+- [ ] Pull-to-refresh and background data sync
261261+262262+### Mobile Phase 3: Notifications & Polish
263263+264264+- [ ] Push notification registration (`expo-notifications` + APNs/FCM)
265265+- [ ] Notification feed screen
266266+- [ ] Appview: `GET /api/notifications` endpoint + push delivery worker
267267+- [ ] Appview: `POST /api/devices` and `DELETE /api/devices/:id` endpoints
268268+- [ ] Deep linking — tap notification to open relevant thread
269269+- [ ] Universal links / custom URI scheme for `at://` URIs
270270+271271+### Mobile Phase 4: Offline & Release
272272+273273+- [ ] Persistent query cache (survive app restarts)
274274+- [ ] Offline thread reading (SQLite cache)
275275+- [ ] Optimistic reply queueing
276276+- [ ] App store assets (icon, screenshots, descriptions)
277277+- [ ] TestFlight (iOS) and internal testing track (Android) release
278278+- [ ] App Store and Google Play submission
279279+280280+---
281281+282282+## API Stability & Compatibility
283283+284284+Mobile clients can't be force-updated instantly, so API stability matters:
285285+286286+- **Additive changes only** — new fields are always optional, never remove existing fields
287287+- **Client version header** — mobile app can send `X-ATBB-Client-Version: 1.2.0`; appview can respond with upgrade-required if the client is too old
288288+- **API versioning** (e.g., `/api/v1/*`) can be introduced later when there's an actual breaking change to warrant it — deferring until post-v1 avoids unnecessary complexity while the API is still evolving
289289+290290+---
291291+292292+## Estimated Effort
293293+294294+| Phase | Scope | Notes |
295295+|---|---|---|
296296+| Phase 0 | Scaffold + navigation shell | Straightforward Expo setup |
297297+| Phase 1 | Read-only browsing + auth | Bulk of the mobile work — screens, auth flow, caching |
298298+| Phase 2 | Write path + interactions | Compose UX, reply sheets, reactions |
299299+| Phase 3 | Push notifications | Requires appview additions (notification service, device registration) |
300300+| Phase 4 | Offline + app store release | Polish, testing, store submission process |
301301+302302+Each phase can be developed and shipped independently. Phase 1 alone is a useful read-only companion app.
303303+304304+---
305305+306306+## Open Questions
307307+308308+1. **Expo vs bare React Native?** Start with Expo managed workflow for speed. Eject to bare only if a native module requires it (unlikely for a forum app).
309309+2. **Code sharing between web and mobile?** The web (server-rendered hypermedia with Hono JSX + HTMX) and mobile (client-side React Native SPA) are fundamentally different paradigms. Component sharing is unlikely to be practical regardless of future web tech choices. Best to share types from `@atbb/lexicon` and API contracts, not UI components.
310310+3. **Moderation UI on mobile?** Admins/mods may want to moderate from their phone. This could be a separate "admin" tab that appears based on role, or deferred to web-only initially.
311311+4. **Multiple forum support?** The app could support connecting to multiple atBB instances (different appview URLs). This aligns with the decentralized nature of AT Protocol but adds complexity. Defer to post-v1.
312312+5. **AT Proto OAuth on mobile maturity?** The OAuth spec for AT Protocol is still evolving. Verify the state of native app support (PKCE, custom URI schemes) and the `@atproto/oauth-client` library's mobile compatibility before starting implementation. DPoP key management specifics covered in Authentication section above.