DuoFrame
A private photo widget. A snap from your person lands on your home screen and disappears after 24 hours.
- Role
- Backend owner, led the app redesign
- Where
- Consumer app
- Year
- 2026
- Stack
- Flutter, Next.js, Supabase, Cloudflare R2
- Status
- Live

DuoFrame widget on an Android home screen showing a friend's latest photo 
Home tab with a streak and the latest snap from a friend 
Photo editor with caption styles and colours 
Received snaps, each showing the hours it has left 
Story viewer with emoji reactions and a reply button 
Onboarding screen: your person, on your home screen
What it is
DuoFrame is for partners, best friends and family who want a private frame instead of another feed. You send a photo, and it shows on the other person's home-screen widget and in the app for 24 hours. Then it's gone.
It's live on Google Play. The API runs on Next.js at Vercel, Postgres and auth are on Supabase, and photos are served from Cloudflare R2.
What I did
- Moved the API from Express to Next.js and TypeScript, with zod validation on every route and row-level security locked down in Postgres.
- Rebuilt the app in October 2026: an offline-first outbox with optimistic sends, a widget-shaped camera, two-person DuoShots taken at the same time, a story viewer with replies, and memories with monthly recaps.
- Built streaks with streak freezes, a daily question, an activity feed with exact deep links, and contact discovery.
- Added a Pro plan and gifting a year of Pro, through RevenueCat.
- Shipped the Play policy work: age gate, account deletion, block and report, child-safety pages.
- Built the marketing site, with widget set-up guides for each phone brand, structured data and IndexNow.
- Wrote contract tests and end-to-end feature checks. CI typechecks and runs them before every deploy.
Hard parts
Private photos without breaking old installs
Photos moved to private storage behind a Cloudflare Worker that checks access on every load. Older app versions don't send a token with image requests, so the API gives them signed links per viewer instead. Old and new apps both kept working, so the backend could ship before the app update.
One upload, several recipients
A multi-recipient send stores one image shared by several snap rows. Unsending, removing a friend or deleting an account removes the rows first and only deletes images that nothing references any more, so one person's unsend never blanks someone else's widget.
Streaks for photos that expire
Snaps are deleted after 24 hours, so streaks come from a permanent send log. Every request carries the phone's time-zone offset, so a day means the user's day, not the server's.
A teammate built the first version of the app. I own the backend and led the October 2026 redesign.