Why this one is worth reading
Banny is a consumer health app in a category most engineers never touch, and the temptation is to describe the product. The part worth an interview is narrower than that: it is what happens when a Flutter app needs authority it cannot have from Dart.
The process boundary
Apple's Screen Time stack is not an API you call. It is a set of app extensions the system loads into their own processes, at times of its choosing, with your app nowhere in the picture. Three of them matter here.
DeviceActivityMonitor is called when a scheduled interval ends. Banny uses it to lift a timed block: the app applies the shield and schedules the interval, and when the interval elapses the system wakes the extension, which clears the ManagedSettingsStore itself. The app does not need to be running, backgrounded, or even installed in memory.
ShieldConfiguration renders what a user sees when they open a blocked app.
ShieldAction decides what happens when they press a button on that screen.
None of these can talk to Flutter. There is no engine in those processes and no method channel to reach. So the contract between the app and its extensions is a shared App Group: the app writes the shield mode and the end timestamp into group user defaults, and the extensions read and clear them. That is a deliberately small surface, and it is the only one that survives the app not existing at the moment the work needs to happen.
Availability, not deployment target
Family Controls needs iOS 16. Raising the project's deployment target to match would have been the quick fix and would have shut out every device below it, for a capability most sessions never invoke.
Every Family Controls call is gated behind an availability check instead. The app compiles and installs on the older target, and on a device that cannot enforce a shield the feature simply is not offered. The rest of the product is unaffected.
The assessment is the product
Recovery programmes that hand everyone the same thirty days are wrong for almost everyone, and visibly arbitrary to anyone who thinks about it for a second.
Banny's onboarding is a branching questionnaire across four axes: physical, mental, social and lifestyle. Its answers feed a scorer that sets the programme length anywhere from fourteen to sixty days against a twenty-one day baseline, weighted by factors with a stated rationale. The programme then moves through phases with different emphases rather than repeating the same daily prompt.
The engineering point is that the number is computed, which means it can be explained to a user and revised against outcomes. A hardcoded thirty can only be defended.
Two codebases, one Supabase project
The Flutter app and a Next.js 16 platform share one Supabase Postgres project with auth and row-level security. The web side carries the marketing site, the content hub and an admin dashboard; the app carries the programme itself. Keeping one database behind both means content written once appears in both places, and means there is no second source of truth to drift.
Reminders that know where the user is
A recovery programme lives or dies on whether people come back on day nine, and a daily "don't forget!" is how an app gets its notifications switched off. So Banny has no generic reminder. It has six specific ones, and each is computed from the user's own state.
Daily pulse, weekly check-in and exercise reminders repeat at the times the user chose, in their local timezone, with the recurrence stored as a cron-style expression so the schedule can be read back and shown rather than lost inside the plugin.
The streak reminder is rebuilt every time a day's adherence is recorded. It fires at seven the next evening, names the exact streak that will break at midnight, and changes its wording as the streak grows. When the streak is zero it is cancelled, because there is nothing to protect.
Reassessment reminders fall at 30, 60 and 90 per cent of the programme the assessment computed, so a fourteen-day plan and a sixty-day plan each get their checkpoints at the right moments, and a checkpoint already completed is never scheduled again.
The trial-end reminder tells the user the day before a free trial converts. For a one-day trial, where "the day before" does not exist, it fires two hours after the trial starts instead. Telling people before they are charged costs a few conversions and buys a lot of trust, and trust is the product in this category.
Underneath, each feature owns a reserved block of notification IDs so one feature can never cancel another's reminder, schedule metadata is persisted so the app can show what is pending after a restart, every payload deep-links to the screen the reminder is about, and ending a session clears every pending reminder.
Measurement
Every analytics event goes through one facade that fans out to PostHog and Firebase Analytics, with Crashlytics attached to the same user identifier. Collection is switched off entirely in the development flavour, so test sessions never pollute the funnel the product decisions are made from.
Around that: RevenueCat for subscriptions and the paywall, Codemagic for builds and store upload, and three locales at launch.