re:mind is built on a modern, production-grade stack chosen for cross-platform reach, offline resilience, real-time sync, and developer velocity. Every layer serves a specific purpose in delivering a responsive, cross-platform experience.
| Layer | Technology | Purpose |
|---|---|---|
| Frontend Framework | React Native 0.81 + Expo SDK 54 | Cross-platform mobile (Android, iOS, Web) |
| Language | TypeScript | Type-safe development |
| Navigation | Expo Router v6 | File-based routing with tabs + stacks |
| Animations | React Native Reanimated v4 | 60fps gesture-driven animations |
| Local Database | TinyBase + expo-sqlite | Offline-first reactive store with SQLite persistence |
| Backend | Convex | Serverless functions + real-time database |
| Authentication | Convex Auth + Resend | Passwordless email OTP |
| Payments | RevenueCat (react-native-purchases) | Subscription management |
| Push Notifications | Expo Push Notifications | Silent sync triggers across devices |
| Recurrence Engine | rrule-temporal | iCalendar RRULE spec (Google Calendar-level) |
| State Management | React Context + Custom Hooks | 24 custom hooks for app logic |
The following diagram illustrates how re:mind's client, backend, and external services connect to form a cohesive, real-time system.
graph TB
subgraph Client["Mobile App (React Native + Expo)"]
UI["UI Layer<br/>Expo Router + Reanimated"]
Store["TinyBase Store<br/>(Reactive State)"]
SQLite["expo-sqlite<br/>(Persistent Storage)"]
RC["RevenueCat SDK<br/>(Purchases)"]
Push["Expo Push<br/>(Notifications)"]
end
subgraph Backend["Convex Cloud"]
API["Serverless Functions<br/>(Queries + Mutations)"]
DB["Real-time Database<br/>(Documents)"]
Auth["Convex Auth<br/>(Email OTP via Resend)"]
Webhook["RC Webhook Handler<br/>(Idempotent)"]
Scheduler["Convex Scheduler<br/>(Background Jobs)"]
end
subgraph External["External Services"]
Resend["Resend<br/>(Email Delivery)"]
RCCloud["RevenueCat<br/>(Subscription Management)"]
PlayStore["Google Play<br/>(Billing)"]
ExpoPush["Expo Push Service"]
end
UI --> Store
Store <--> SQLite
Store <--> API
API <--> DB
Auth --> Resend
RC <--> RCCloud
RCCloud --> Webhook
RCCloud <--> PlayStore
Webhook --> DB
Scheduler --> ExpoPush
Push --> ExpoPush
The app uses an offline-first architecture where TinyBase provides a reactive in-memory store synced to SQLite for persistence. All reads and writes happen against TinyBase first, guaranteeing instant UI response regardless of network state.
Convex provides the cloud backend with real-time subscriptions. When data changes server-side, connected clients receive updates automatically through Convex's reactive query system, with no polling required.
The sync layer uses a pull/push model: pull on app launch plus periodic sync, push on each local mutation. This ensures data converges without manual conflict resolution in the vast majority of cases.
Silent push notifications trigger cross-device sync without user-visible notifications. When a user edits a reminder on one device, all other devices receive a silent push that triggers an immediate data pull from Convex.
The core design principle is that the user should never wait for the network. Every interaction is local-first, with sync happening transparently in the background.
sequenceDiagram
participant User
participant App as Mobile App
participant SQLite as SQLite (Local)
participant TinyBase as TinyBase Store
participant Convex as Convex Cloud
participant OtherDevice as Other Devices
User->>App: Create/Edit Reminder
App->>TinyBase: Write to reactive store
TinyBase->>SQLite: Persist locally
App-->>User: Instant UI update
TinyBase->>Convex: Push mutation
Convex->>Convex: Store in database
Convex->>OtherDevice: Silent push notification
OtherDevice->>Convex: Pull updated data
OtherDevice->>OtherDevice: Update local store
Subscription management is handled end-to-end by RevenueCat, with a client-side SDK integration and a server-side webhook handler for reliable state synchronization.
The RevenueCat SDK is initialized using a state machine pattern implemented
in revenueCatState.ts. Three transitions govern the SDK lifecycle:
markRcConfigured(): called after Purchases.configure()
completes successfully
markRcUnavailable(): called if configuration fails (e.g., on unsupported
platforms or simulator environments)
rcReady(): returns a promise that resolves when the SDK is fully initialized
All RevenueCat SDK calls await rcReady() before proceeding.
This prevents race conditions where purchase or restore calls execute before the SDK is
configured, which would otherwise throw runtime errors.
The subscription screen fetches available offerings from RevenueCat, displays monthly and
annual plans with localized pricing, and handles both new purchases and subscription
restores. Entitlement checks are performed through the isPro flag exposed by
ThemeContext, which gates premium features including unlimited reminders and
all five color themes.
RevenueCat sends server-to-server webhooks for every subscription lifecycle event. The Convex webhook handler processes these with full idempotency and error resilience.
flowchart TD
A["RevenueCat Event"] --> B{"Auth Header Valid?"}
B -->|No| C["401 Unauthorized"]
B -->|Yes| D{"Parse JSON Body"}
D -->|Malformed| E["400 Bad Request"]
D -->|Valid| F{"Event ID Seen Before?"}
F -->|Yes| G["200 OK (Skip - Idempotent)"]
F -->|No| H{"Event Type?"}
H -->|INITIAL_PURCHASE\nRENEWAL\nUNCANCELLATION| I["Set status: active"]
H -->|CANCELLATION| J["Set status: cancelled"]
H -->|EXPIRATION| K["Set status: expired"]
H -->|BILLING_ISSUE| L["Keep active + log warning"]
H -->|PRODUCT_CHANGE| M["Set status: active\nUpdate entitlements"]
I & J & K & L & M --> N{"Mutation Success?"}
N -->|No| O["500 (RC will retry)"]
N -->|Yes| P["Store Event ID"]
P --> Q["200 OK"]
The webhook handler is idempotent; duplicate events from RevenueCat
retries are safely skipped. Each incoming event's unique ID is checked against a
webhookEvents table in Convex. If the event has already been processed, the
handler returns 200 OK immediately without re-executing the mutation.
Authorization is verified via a shared secret passed in the HTTP header. Invalid requests
are rejected with 401 Unauthorized before any processing occurs.
If a mutation fails (database error, transient issue), the handler returns
500 Internal Server Error. RevenueCat's retry mechanism will then re-deliver
the event up to 5 times at exponentially increasing intervals: 5, 10, 20, 40, and 80
minutes. This guarantees eventual delivery even during temporary outages.
The user's subscription status and entitlements are updated directly in the Convex database. Because Convex provides real-time subscriptions, the client immediately reflects the new subscription state without requiring a manual refresh or app restart.
| Aspect | Detail |
|---|---|
| Entitlement | re:mind:pro |
| Monthly Product | re_mind_pro:monthly-autorenewing (GBP 3.99) |
| Annual Product | re_mind_pro:annual-autorenewing (GBP 29.99) |
| Free Tier Limit | 5 active reminders, 2 themes |
| Pro Features | Unlimited reminders, all 5 themes |
| PPP Pricing | 165 countries with localized prices |
| Offering | default (current) |
Notable engineering decisions in re:mind:
rrule-temporal, supporting patterns like "every 3rd Wednesday" or "MWF for
10 weeks." This is the same recurrence standard used by Google Calendar.