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

getduoframe.com

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.