Download

Download on the App Store

Technical documentation

🛠️ Tech stack

📱 Mobile app

  • Amplitude, Sentry — analytics and error tracking
  • Expo Notifications — push; device tokens stored in backend
  • RevenueCat (react-native-purchases) — subscriptions and IAP
  • TanStack Query — server state; Expo Router + React Navigation for navigation
  • AWS Amplify — GraphQL client and Cognito (Apple / Google sign-in)
  • React Native (Expo SDK 55, bare workflow), TypeScript

⚙️ Backend

  • Expo Push API — sending push notifications from Lambda
  • SQS — reminder notification queue; EventBridge — 1-minute schedule for enqueue
  • DynamoDB — users, reminders, event-state, ideas
  • AWS AppSync — GraphQL API, Cognito User Pools auth
  • AWS Lambda (Node.js 22) + Serverless Framework

☁️ 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.