WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

Address PR #4 review comments

Fix critical issues:
- Fix OAuth architecture: remove appview mediation endpoints, rewrite auth
flow to have mobile app exchange tokens directly with user's PDS and
present DPoP-bound tokens to appview (preserves AT Proto decentralization)
- Update iOS PWA claim: reflect iOS 16.4+ Web Push support with accurate
constraints (requires add-to-home-screen, constrained UX)
- Add missing reactions endpoints to API inventory with note about DB/lexicon
gaps
- Remove premature API versioning: defer /api/v1/* until post-v1 to avoid
breaking changes while API still evolving

Improvements per review suggestions:
- Elevate DPoP key management to dedicated Authentication subsection with
mobile-specific secure storage details (secure enclave/keystore)
- Clarify devices table is local/appview-managed, not AT Proto record
- Clarify mobile build pipeline: Metro bundler (expo/eas build) vs Turborepo
(lexicon types used at dev/typecheck only)
- Reframe component sharing question: web and mobile paradigms fundamentally
different, share types/contracts not UI components

+36 -25
+36 -25
docs/mobile-apps-plan.md
··· 51 51 52 52 | Endpoint / Feature | Purpose | 53 53 |---|---| 54 - | `POST /api/auth/oauth/start` | Initiate AT Proto OAuth (return redirect URL for in-app browser) | 55 - | `POST /api/auth/oauth/callback` | Exchange code for session token | 56 54 | `GET /api/users/:did` | User profile / post history | 57 55 | `GET /api/notifications` | Notification feed (replies to your posts, mentions, mod actions) | 56 + | `POST /api/reactions` | Add reaction to a post (requires `reactions` table + `space.atbb.reaction` lexicon) | 57 + | `DELETE /api/reactions/:id` | Remove reaction from a post | 58 58 | `POST /api/devices` | Register push notification token (APNs / FCM) | 59 59 | `DELETE /api/devices/:id` | Unregister push token | 60 60 | Pagination headers / cursors | Consistent cursor-based pagination across all list endpoints | ··· 83 83 |---|---| 84 84 | Flutter | Dart — different language from the rest of the stack, can't share types | 85 85 | Native (Swift/Kotlin) | Two codebases to maintain, slower iteration for a small team | 86 - | PWA only | No push notifications on iOS (limited), no app store presence, weaker offline | 86 + | PWA only | iOS Web Push requires add-to-home-screen with constrained UX, no app store presence, weaker offline | 87 87 | Capacitor/Ionic | WebView wrapper — won't feel native, performance ceiling | 88 88 89 89 ### Monorepo integration ··· 98 98 mobile/ # NEW — React Native + Expo app 99 99 ``` 100 100 101 - The mobile package imports `@atbb/lexicon` for type safety against AT Protocol records. The Turborepo build pipeline extends naturally — `mobile` depends on `lexicon` types just like `web` and `appview` do. 101 + 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. 102 102 103 103 --- 104 104 ··· 108 108 109 109 | Screen | Description | API | 110 110 |---|---|---| 111 - | **Login** | AT Proto OAuth flow via in-app browser | `/api/auth/oauth/*` | 111 + | **Login** | AT Proto OAuth flow via in-app browser (exchanges tokens with user's PDS) | `@atproto/oauth-client` | 112 112 | **Home** | Category list, forum branding | `GET /api/categories` | 113 113 | **Category** | Topic list with pull-to-refresh, infinite scroll | `GET /api/categories/:id/topics` | 114 114 | **Topic/Thread** | OP + flat replies, pagination | `GET /api/topics/:id` | ··· 138 138 | State / cache | TanStack Query (React Query) — handles caching, pagination, background refetch | 139 139 | Push notifications | `expo-notifications` + server-side APNs/FCM | 140 140 | Secure storage | `expo-secure-store` (for auth tokens) | 141 - | Auth browser | `expo-auth-session` or `expo-web-browser` (for OAuth redirect) | 141 + | AT Proto OAuth | `@atproto/oauth-client` (client-side OAuth + DPoP) + `expo-auth-session` (in-app browser) | 142 142 | Offline storage | SQLite via `expo-sqlite` (cache threads for offline reading) | 143 143 144 144 --- 145 145 146 146 ## Authentication on Mobile 147 147 148 - AT Proto OAuth on mobile follows the standard OAuth 2.0 + PKCE flow adapted for native apps: 148 + AT Proto OAuth on mobile follows the standard OAuth 2.0 + PKCE + DPoP flow for native apps: 149 149 150 150 1. User enters their handle or PDS URL 151 - 2. App resolves the user's PDS and authorization server 152 - 3. App opens an in-app browser (ASWebAuthenticationSession on iOS, Custom Tab on Android) to the authorization URL with a PKCE challenge 153 - 4. User authenticates on their PDS 154 - 5. PDS redirects back to the app via a custom URI scheme (`atbb://oauth/callback`) or universal link 155 - 6. App exchanges the authorization code for tokens via the appview's callback endpoint 156 - 7. Tokens stored in `expo-secure-store` (keychain on iOS, keystore on Android) 157 - 8. Subsequent API calls include the bearer token 151 + 2. App resolves the user's PDS and authorization server from the user's DID document 152 + 3. App generates a DPoP key pair (stored in secure enclave/keystore) and creates a PKCE challenge 153 + 4. App opens an in-app browser (ASWebAuthenticationSession on iOS, Custom Tab on Android) to the authorization URL 154 + 5. User authenticates on their PDS 155 + 6. PDS redirects back to the app via a custom URI scheme (`atbb://oauth/callback`) or universal link 156 + 7. **App exchanges the authorization code directly with the user's PDS authorization server** (not via the appview) to obtain access/refresh tokens 157 + 8. Tokens stored in `expo-secure-store` (keychain on iOS, keystore on Android) 158 + 9. Subsequent API calls to the appview include a DPoP-bound bearer token 159 + 10. **The appview validates tokens against the user's DID document** — it doesn't broker authentication 160 + 161 + 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). 162 + 163 + ### DPoP Key Management on Mobile 164 + 165 + 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: 166 + 167 + - **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. 168 + - **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. 169 + - **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. 158 170 159 - The appview mediates this flow — the mobile app doesn't need to implement AT Proto session management directly. 171 + 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). 160 172 161 173 --- 162 174 ··· 188 200 ### Implementation 189 201 190 202 - Mobile app registers its push token with `POST /api/devices` on login 191 - - Appview maintains a `devices` table: `(id, user_did, platform, push_token, created_at)` 203 + - 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 192 204 - 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 193 205 - A lightweight push service (can be part of the appview or a separate worker) sends the notification payload 194 206 - Start simple: notify on direct replies only. Expand to mentions, mod actions, thread subscriptions later ··· 232 244 233 245 ### Mobile Phase 1: Read-Only Browse 234 246 235 - - [ ] Login screen — AT Proto OAuth via `expo-auth-session` 247 + - [ ] Login screen — AT Proto OAuth client (`@atproto/oauth-client` + `expo-auth-session`) with direct PDS token exchange 236 248 - [ ] Home screen — category list from `GET /api/categories` 237 249 - [ ] Category screen — topic list with pull-to-refresh and infinite scroll 238 250 - [ ] Thread screen — OP + flat replies with pagination ··· 267 279 268 280 --- 269 281 270 - ## API Versioning & Compatibility 282 + ## API Stability & Compatibility 271 283 272 - To support mobile clients that can't be force-updated instantly: 284 + Mobile clients can't be force-updated instantly, so API stability matters: 273 285 274 - - **Version the API** with a prefix: `/api/v1/categories`, `/api/v1/topics/:id`, etc. The current unversioned endpoints become v1. 275 - - **Additive changes only** within a version — new fields are always optional, never remove fields 276 - - **Minimum version header** — mobile app sends `X-ATBB-Client-Version: 1.2.0`; appview can respond with upgrade-required if the client is too old 277 - - **Deprecation period** — old API versions stay live for at least 6 months after a new version ships 286 + - **Additive changes only** — new fields are always optional, never remove existing fields 287 + - **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 288 + - **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 278 289 279 290 --- 280 291 ··· 295 306 ## Open Questions 296 307 297 308 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). 298 - 2. **Shared component library?** If the web UI eventually moves to React (from Hono JSX + HTMX), a shared component package could serve both. For now, keep them separate — the web and mobile UX patterns are different enough. 309 + 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. 299 310 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. 300 311 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. 301 - 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, DPoP) before starting implementation. 312 + 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.