One template, fourteen apps
How a single Flutter template and one shared AI backend turned into a family of photo-identifier apps.
- Period
- 2025 – 2026
- Scope
- 14 apps · 1 backend
- Stack
- Flutter, Node.js, Express, Gemini, RevenueCat, Fly.io
- Status
- Several live on the App Store
Problem
Photo-identifier apps all have the same shape. Take a picture, get an answer, keep it in a library, ask a follow-up question. What changes is the subject: a rock, a coin, a mushroom, a stamp.
I wanted to find out which subjects people actually care about. Building each app from scratch would have taken months per guess, so I needed each new app to cost days instead.
Approach
One app template. Nine apps started from the same Flutter template on the same day. The template held everything subject-agnostic: camera, library, detail screen, AI chat about an item, onboarding, paywall, caching and connectivity handling. Each app only swapped its theme, copy and prompts.
One backend for everything. Instead of each app calling an AI provider directly, they all talk to one small Node service. It holds the API keys, rate-limits by client IP and turns a model response into a stable contract the apps can rely on. It now serves more than the identifiers, including book recommendations, affirmations, recipe extraction and food logging for my other apps.
Frameworkize the second time, not the first. The first routes were copy-pasted per subject, each with its own prompt, parsing and error handling. Once a few apps were live and the differences were clear, I pulled the shared flow into one function. Now a new subject is mostly configuration:
registerIdentifyRoute(app, deps, {
path: "/identify-stamp",
// Step 1: cheap yes/no check so a photo of a cat
// doesn't get "identified" as a rare stamp.
checkPrompt: STAMP_CHECK_PROMPT,
checkKey: "containsStamp",
rejectMessage: "That doesn't look like a stamp.",
// Step 2: the actual identification.
identifyPrompt: STAMP_IDENTIFY_PROMPT,
fallbackTitle: "Unknown stamp",
});
Every route gets the same things for free:
- a two-step check-then-identify flow
- JSON mode with a fallback parser for when the model wraps its answer in prose or code fences
- confidence normalised to 0–1, whether the model says
0.8,80or"high" - error codes that tell the app what went wrong without leaking provider details
- a request ID for tracing a single failed scan
Outcome
- 14 identifier apps built, several of them shipped to the App Store
- a new subject went from weeks of work to a few days, most of it spent on prompts and design
- a second-generation template in 2026 with a proper design system, and mock and remote identifier services so the whole UI runs without a backend
- one backend on one small machine, with API keys that never ship inside an app
The lesson I keep: the first version of anything should be copied freely. The abstraction is only worth building once you've seen three or four real cases and know which parts actually vary.