Architecture Overview
Softly is split into two repositories that communicate over a JSON REST API.
System Diagram
┌─────────────────────┐ ┌──────────────────────┐
│ Mobile (Expo) │ │ Backend (Rails) │
│ │ HTTP │ │
│ React Native + │◄───────►│ Rails 8.1 API │
│ TypeScript │ JSON │ PostgreSQL │
│ Expo Router │ │ Solid Queue │
│ │ │ │
│ RevenueCat SDK ────┼────────►│ RevenueCat Webhook │
│ Deepgram WS ──────┼──┐ │ │
│ expo-notifications │ │ │ Expo Push API ──────┼──► Apple/Google
│ │ │ │ Anthropic API ──────┼──► Claude Haiku
└─────────────────────┘ │ │ Deepgram API ───────┼──► Temp keys
│ └──────────────────────┘
│
└──► Deepgram (direct streaming)Repositories
| Repo | Tech | Purpose |
|---|---|---|
ai-softly | Rails 8.1, PostgreSQL, Solid Queue | API server, background jobs, AI orchestration |
ai-softly-frontend | Expo (React Native), TypeScript | iOS and Android mobile app |
Key Design Decisions
API-only Rails -- No views, no assets. The backend is purely a JSON API consumed by the mobile app.
Solid Queue over Redis -- Background jobs use Solid Queue (database-backed) instead of Sidekiq/Redis. One fewer infrastructure dependency.
Client-side Deepgram -- Voice audio streams directly from the device to Deepgram via WebSocket. The backend only issues temporary API keys. This avoids proxying audio through the server.
RevenueCat for billing -- All App Store/Play Store interactions are handled by RevenueCat. The backend only syncs the resulting subscription state.
React Query for state -- The mobile app uses React Query (TanStack Query) for server state instead of Redux or Zustand. Cache invalidation and optimistic updates are handled through query keys.
File-based routing -- Expo Router provides file-based routing similar to Next.js. Screen paths map directly to files in the app/ directory.
Data Flow
- User interacts with the mobile app
- React Query fires an API request (via Axios in
services/api.ts) - JWT access token is attached via interceptor
- Rails controller processes the request, interacts with models/services
- JSON response is returned
- React Query caches the response and re-renders affected components
For background work (push notifications, subscription sync, weekly digest), Solid Queue processes jobs asynchronously.