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

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/api runs 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 lexiconappview/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
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:

  1. User enters their handle or PDS URL
  2. App resolves the user's PDS and authorization server from the user's DID document
  3. App generates a DPoP key pair (stored in secure enclave/keystore) and creates a PKCE challenge
  4. App opens an in-app browser (ASWebAuthenticationSession on iOS, Custom Tab on Android) to the authorization URL
  5. User authenticates on their PDS
  6. PDS redirects back to the app via a custom URI scheme (atbb://oauth/callback) or universal link
  7. App exchanges the authorization code directly with the user's PDS authorization server (not via the appview) to obtain access/refresh tokens
  8. Tokens stored in expo-secure-store (keychain on iOS, keystore on Android)
  9. Subsequent API calls to the appview include a DPoP-bound bearer token
  10. 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-store or 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-client library 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/devices on login
  • 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
  • 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:

  1. HTTP cache — TanStack Query caches API responses in memory with configurable stale times
  2. Persistent cache — TanStack Query's persistQueryClient with AsyncStorage or SQLite for across-app-restart caching
  3. Explicit offline mode — "Save thread for offline" downloads thread data to SQLite; viewable without network
  4. 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/mobile with Expo + TypeScript template
  • Configure workspace — @atbb/lexicon as 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/notifications endpoint + push delivery worker
  • Appview: POST /api/devices and DELETE /api/devices/:id endpoints
  • 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#

  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).
  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.
  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.
  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.
  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.