Technical documentation
📦 Tech stack
📱 Mobile app
- React Native (Expo SDK 55, bare workflow), TypeScript
- AWS Amplify — GraphQL client and Cognito (Apple / Google sign-in)
- TanStack Query — server state; Expo Router + React Navigation for navigation
- RevenueCat (react-native-purchases) — subscriptions and IAP
- Expo Notifications — push; device tokens stored in backend
- Amplitude, Sentry — analytics and error tracking
⚙️ Backend
- AWS Lambda (Node.js 22) + Serverless Framework
- AWS AppSync — GraphQL API, Cognito User Pools auth
- DynamoDB — users, reminders, event-state, ideas
- SQS — reminder notification queue; EventBridge — 1‑minute schedule for enqueue
- Expo Push API — sending push notifications from Lambda
🏗️ Infrastructure
CloudFormation — DynamoDB tables, SQS, Cognito User Pool and identity providers (Apple, Google). Deployed separately; Serverless imports queue URL and Cognito IDs.
🏛️ Architecture
Backend is the source of truth for reminder and user state; notifications are derived from it.
AppSync is used only for GraphQL queries/mutations and subscriptions (real-time reminder and user updates). No scheduling or recurrence logic in AppSync.
Lambda holds all business logic: CRUD reminders, snooze/complete, recurrence (next occurrence). Conflict rule: last write wins (server updatedAt).
🔔 Reminders
One active instance per reminder (nextTriggerAt, rrule, timezone, state). No occurrence history; a separate event-state table drives which notifications are due.
⏱️ Scheduling
Every minute, a Lambda is triggered to find due events and enqueue messages to SQS. A second Lambda consumes SQS, checks user mute and device tokens, then sends Expo Push notifications (copy from personality module). Sync across devices is via AppSync subscriptions and backend state, not device-specific reminder state.
💳 RevenueCat implementation
RevenueCat is the source of truth for premium. The app uses one entitlement, premium; the backend stores User.isPremium and User.premiumExpiresAt for server-side gating (reminder limit, recurrence, personalities, ideas board, account linking).
📲 App
RevenueCat is configured with Cognito sub as app_user_id. After sign-in (and after purchase/restore), the app reads CustomerInfo and, if premium state changed, calls the backend updateIsPremium mutation with canonical userId so the API stays in sync.
🔗 Backend webhook
POST /webhook/revenuecat (Lambda). Verifies HMAC-SHA256 with REVENUECAT_WEBHOOK_SECRET. On events like INITIAL_PURCHASE, RENEWAL, TRIAL_STARTED sets user premium (and expiresAt); on CANCELLATION / EXPIRATION sets non‑premium. User is resolved by Cognito sub (app_user_id) to the canonical user record; only isPremium and premiumExpiresAt are updated.
🚦 Gating
Resolvers read User.isPremium (and optional premiumExpiresAt); no backend call to RevenueCat. Webhook + app sync keep backend state aligned with RevenueCat.