atBB Mobile Apps — Plan#
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.
Why Mobile Apps?#
The web UI (@atbb/web) will be responsive and mobile-friendly, but dedicated apps offer:
- Push notifications for replies, mentions, and moderation events
- Native performance — smooth scrolling through long threads, instant navigation
- Offline reading — cache threads and categories for subway/airplane use
- Deep OS integration — share sheets, AT Proto URI handling, biometric auth
- Better compose experience — native keyboard handling, image picker, draft persistence
The web app remains the primary interface. Mobile apps are a complement, not a replacement.
Architecture Fit#
The existing system already separates concerns in a way that supports mobile clients:
┌─────────────┐
│ Forum UI │──────┐
│ (Web App) │ │
└─────────────┘ │ ┌──────────────┐ ┌─────────────────┐
├────▶│ AppView │────▶│ Firehose / │
┌─────────────┐ │ │ (JSON API) │◀────│ User PDS nodes │
│ Mobile Apps │──────┘ └──────────────┘ └─────────────────┘
│ (iOS/Andrd) │
└─────────────┘
Mobile apps consume the same /api/* endpoints as the web UI. No new backend is needed — just the existing appview.
What the appview already provides#
| Endpoint | Purpose | Mobile use |
|---|---|---|
GET /api/forum |
Forum metadata | App title, description, branding |
GET /api/categories |
Category list | Home screen / tab bar |
GET /api/categories/:id/topics |
Topic list (paginated) | Category view with pull-to-refresh |
GET /api/topics/:id |
Thread (OP + replies) | Thread view |
POST /api/topics |
Create topic | Compose screen |
POST /api/posts |
Create reply | Reply sheet |
What needs to be added to the appview for mobile#
| Endpoint / Feature | Purpose |
|---|---|
GET /api/users/:did |
User profile / post history |
GET /api/notifications |
Notification feed (replies to your posts, mentions, mod actions) |
POST /api/reactions |
Add reaction to a post (requires reactions table + space.atbb.reaction lexicon) |
DELETE /api/reactions/:id |
Remove reaction from a post |
POST /api/devices |
Register push notification token (APNs / FCM) |
DELETE /api/devices/:id |
Unregister push token |
| Pagination headers / cursors | Consistent cursor-based pagination across all list endpoints |
ETag / Last-Modified headers |
Conditional requests for efficient caching |
These additions benefit the web UI too — they aren't mobile-only concerns.
Technology Choice: React Native + Expo#
Recommendation: React Native with Expo for cross-platform iOS and Android from a single codebase.
Why React Native + Expo#
- Single codebase for iOS and Android — critical for a small team / solo developer
- TypeScript — same language as the rest of the monorepo; can share types from
@atbb/lexicon - Expo simplifies builds, OTA updates, push notifications, and app store submissions
- Mature ecosystem — navigation (React Navigation / Expo Router), state management, networking
- AT Proto libraries work —
@atproto/apiruns in React Native with minor polyfills - AGPL-3.0 compatible — React Native's MIT license is compatible with the project license
Why not other options#
| Option | Reason to skip |
|---|---|
| Flutter | Dart — different language from the rest of the stack, can't share types |
| Native (Swift/Kotlin) | Two codebases to maintain, slower iteration for a small team |
| PWA only | iOS Web Push requires add-to-home-screen with constrained UX, no app store presence, weaker offline |
| Capacitor/Ionic | WebView wrapper — won't feel native, performance ceiling |
Monorepo integration#
Add a new package to the existing workspace:
packages/
lexicon/ # shared types (already exists)
appview/ # JSON API (already exists)
web/ # server-rendered UI (already exists)
mobile/ # NEW — React Native + Expo app
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.
Mobile App Structure#
Screens#
| Screen | Description | API |
|---|---|---|
| Login | AT Proto OAuth flow via in-app browser (exchanges tokens with user's PDS) | @atproto/oauth-client |
| Home | Category list, forum branding | GET /api/categories |
| Category | Topic list with pull-to-refresh, infinite scroll | GET /api/categories/:id/topics |
| Topic/Thread | OP + flat replies, pagination | GET /api/topics/:id |
| Compose | New topic form (select category, write post) | POST /api/topics |
| Reply | Reply sheet (bottom sheet or modal) | POST /api/posts |
| Notifications | Reply/mention/mod action feed | GET /api/notifications |
| Profile | User info, post history | GET /api/users/:did |
| Settings | Push notification prefs, theme, logout | Local + /api/devices |
Navigation#
Tab Bar
├── Home (categories → topics → thread)
├── Notifications
└── Profile / Settings
Use Expo Router (file-based routing) or React Navigation with a bottom tab + stack pattern.
Key Libraries#
| Concern | Library |
|---|---|
| Navigation | Expo Router or React Navigation |
| HTTP client | Standard fetch or ky (lightweight) |
| State / cache | TanStack Query (React Query) — handles caching, pagination, background refetch |
| Push notifications | expo-notifications + server-side APNs/FCM |
| Secure storage | expo-secure-store (for auth tokens) |
| AT Proto OAuth | @atproto/oauth-client (client-side OAuth + DPoP) + expo-auth-session (in-app browser) |
| Offline storage | SQLite via expo-sqlite (cache threads for offline reading) |
Authentication on Mobile#
AT Proto OAuth on mobile follows the standard OAuth 2.0 + PKCE + DPoP flow for native apps:
- User enters their handle or PDS URL
- App resolves the user's PDS and authorization server from the user's DID document
- App generates a DPoP key pair (stored in secure enclave/keystore) and creates a PKCE challenge
- App opens an in-app browser (ASWebAuthenticationSession on iOS, Custom Tab on Android) to the authorization URL
- User authenticates on their PDS
- PDS redirects back to the app via a custom URI scheme (
atbb://oauth/callback) or universal link - App exchanges the authorization code directly with the user's PDS authorization server (not via the appview) to obtain access/refresh tokens
- Tokens stored in
expo-secure-store(keychain on iOS, keystore on Android) - Subsequent API calls to the appview include a DPoP-bound bearer token
- The appview validates tokens against the user's DID document — it doesn't broker authentication
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).
DPoP Key Management on Mobile#
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:
- 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-storeor platform-specific crypto APIs. - 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.
- Proof generation: For each API request, generate a DPoP proof (signed JWT) using the private key. The
@atproto/oauth-clientlibrary handles this, but mobile-specific integration with secure storage may require custom bindings.
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).
Push Notifications#
Architecture#
User posts a reply
│
▼
Firehose event
│
▼
AppView indexes reply
│
▼
Notification service checks:
"Who should be notified?"
│
▼
Sends push via APNs (iOS)
and/or FCM (Android)
│
▼
Mobile device shows notification
Implementation#
- Mobile app registers its push token with
POST /api/deviceson login - Appview maintains a
devicestable:(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 - 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
- A lightweight push service (can be part of the appview or a separate worker) sends the notification payload
- Start simple: notify on direct replies only. Expand to mentions, mod actions, thread subscriptions later
Notification types (phased)#
| Phase | Notification |
|---|---|
| Initial | Direct reply to your post |
| Later | @mention in a post |
| Later | Mod action on your post (locked, deleted) |
| Later | New topic in a subscribed category |
| Later | Thread subscription (get notified on any reply in a thread) |
Offline Support#
Use a layered caching strategy:
- HTTP cache — TanStack Query caches API responses in memory with configurable stale times
- Persistent cache — TanStack Query's
persistQueryClientwith AsyncStorage or SQLite for across-app-restart caching - Explicit offline mode — "Save thread for offline" downloads thread data to SQLite; viewable without network
- Optimistic writes — compose a reply offline, queue it, send when back online
MVP mobile app only needs layer 1 (in-memory cache). Layers 2-4 come later.
Implementation Phases#
Mobile Phase 0: Scaffold (after web MVP Phase 4+)#
Prerequisites: Appview API is stable with read endpoints working, AT Proto OAuth is implemented.
- Add
packages/mobilewith Expo + TypeScript template - Configure workspace —
@atbb/lexiconas dependency for shared types - Set up Expo Router navigation structure (tabs + stacks)
- Implement API client layer using
fetch+ TanStack Query - Create basic UI shell: tab bar, placeholder screens
Mobile Phase 1: Read-Only Browse#
- Login screen — AT Proto OAuth client (
@atproto/oauth-client+expo-auth-session) with direct PDS token exchange - Home screen — category list from
GET /api/categories - Category screen — topic list with pull-to-refresh and infinite scroll
- Thread screen — OP + flat replies with pagination
- Basic theming (light/dark mode following system preference)
- Loading states, error states, empty states
Mobile Phase 2: Write & Interact#
- Compose screen — create new topic (select category, write text)
- Reply sheet — reply to a post (bottom sheet UX)
- Reactions — tap to react on posts
- Profile screen — view your own posts and membership info
- Pull-to-refresh and background data sync
Mobile Phase 3: Notifications & Polish#
- Push notification registration (
expo-notifications+ APNs/FCM) - Notification feed screen
- Appview:
GET /api/notificationsendpoint + push delivery worker - Appview:
POST /api/devicesandDELETE /api/devices/:idendpoints - Deep linking — tap notification to open relevant thread
- Universal links / custom URI scheme for
at://URIs
Mobile Phase 4: Offline & Release#
- Persistent query cache (survive app restarts)
- Offline thread reading (SQLite cache)
- Optimistic reply queueing
- App store assets (icon, screenshots, descriptions)
- TestFlight (iOS) and internal testing track (Android) release
- App Store and Google Play submission
API Stability & Compatibility#
Mobile clients can't be force-updated instantly, so API stability matters:
- Additive changes only — new fields are always optional, never remove existing fields
- 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 - 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
Estimated Effort#
| Phase | Scope | Notes |
|---|---|---|
| Phase 0 | Scaffold + navigation shell | Straightforward Expo setup |
| Phase 1 | Read-only browsing + auth | Bulk of the mobile work — screens, auth flow, caching |
| Phase 2 | Write path + interactions | Compose UX, reply sheets, reactions |
| Phase 3 | Push notifications | Requires appview additions (notification service, device registration) |
| Phase 4 | Offline + app store release | Polish, testing, store submission process |
Each phase can be developed and shipped independently. Phase 1 alone is a useful read-only companion app.
Open Questions#
- 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).
- 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/lexiconand API contracts, not UI components. - 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.
- 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.
- 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-clientlibrary's mobile compatibility before starting implementation. DPoP key management specifics covered in Authentication section above.