PinkyPact
Inspiration
My todo list was never the problem. Procrastination was. The things that mattered most were the easiest to push to tomorrow, and nothing on my phone actually cared whether I did them. One thing always worked, though: telling another person I would do it. So I built the smallest possible version of that, for exactly two people. Promises that actually count, because someone you know is waiting on the other end.
What it does
You give your partner a task with a deadline. If they miss it, nagging starts: a push every 30 minutes, plus a screen in the app they cannot dismiss until it is resolved. There is no auto-confirm anywhere. The assignee reports "done", and only the other person can approve it, or reject it with a comment that sends the task straight back to overdue. If they sit on the review, the nagging turns around and targets them instead.
How I built it
Expo and React Native (strict TypeScript), Firestore for data, Firebase anonymous auth so there is no signup screen at all, Expo Push for delivery, App Check on every callable, and RevenueCat for the paid tier.
Cloud Functions run the parts that must not be forgeable or missed: task creation, the overdue and review sweeps, and both nagging tracks. The state machine itself is a shared module, and the transitions the client performs are validated by Firestore rules.
PinkyPact Plus is a subscription (monthly or annual) that lets you each keep three promises open at once, and it is couple-wide: whoever pays, both partners get it. Every new couple starts with 14 free days of it, through Apple's introductory offer, so the app has to ask for money exactly once and never before the two of them are actually connected.
The interesting part is that the client never decides entitlement. RevenueCat's webhook is treated as a trigger, not as evidence, so the backend ignores the payload and re-fetches the subscriber over the REST API with a secret key that never ships in the binary. Task creation moved out of the client into a callable that counts open promises inside a Firestore transaction, and the rules say allow create: if false on tasks. A tampered client can lie to its own UI all it likes; the document simply never gets written.
What I learned
Identity is not final at launch. This app has no accounts, so a user restoring a lost phone spends the first minute of the session under a throwaway anonymous uid they are about to abandon. Hand that uid to RevenueCat, and with transfer-on-restore a paying user's purchase can migrate onto an account nobody can ever sign into again. The rule I ended up with: RevenueCat is only ever told about a uid that has settled.
The best bug of the build. With App Check enforced and no startup gate, cold-start reads failed permanently while writes kept succeeding. The listener was poisoned before the first token existed, so it looked random and intermittent, and "but it saved fine" was never the counter-evidence it seemed to be.
No OTA updates. Every JS fix needs a native build, which quietly changes how you triage: you batch, you verify harder, and you stop treating a one-line fix as free. It also changes the order you ship in. A rule the server enforces and a rule the app displays are compiled from the same constant into two different binaries, so tightening one before the other is how you break people who have not updated yet.
Status
Live on the App Store. Version 1.1.0, the one that puts the first 14 days on the house, shipped.
Built With
- cloud-firestore
- eas
- expo-notifications
- expo-router
- expo.io
- firebase
- firebase-app-check
- firebase-authentication
- firebase-cloud-functions
- in-app-purchase
- ios
- node.js
- posthog
- react
- react-native
- revenuecat
- sentry
- storekit
- typescript
Log in or sign up for Devpost to join the conversation.