<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cesare Bramante</title>
    <description>The latest articles on DEV Community by Cesare Bramante (@cbramante).</description>
    <link>https://dev.to/cbramante</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4150476%2Fe9797120-746b-4a69-911f-85df4de6afcc.png</url>
      <title>DEV Community: Cesare Bramante</title>
      <link>https://dev.to/cbramante</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cbramante"/>
    <language>en</language>
    <item>
      <title>Several AIs, one app: the rules I wrote because they all remember things wrong</title>
      <dc:creator>Cesare Bramante</dc:creator>
      <pubDate>Thu, 12 Nov 2026 10:36:59 +0000</pubDate>
      <link>https://dev.to/cbramante/several-ais-one-app-the-rules-i-wrote-because-they-all-remember-things-wrong-2bfm</link>
      <guid>https://dev.to/cbramante/several-ais-one-app-the-rules-i-wrote-because-they-all-remember-things-wrong-2bfm</guid>
      <description>&lt;p&gt;I built a native app with several AIs. The most useful thing I wrote in the whole project wasn't code. It was a short list of rules about not trusting their memory.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/cbramante/most-of-my-bugs-started-with-a-word-not-with-code-51p5"&gt;last episode&lt;/a&gt; I told how most of my bugs started with a word and not with code. This one is about working with more than one AI: who did what, what went wrong and the method that came out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who did what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;January: Gemini.&lt;/strong&gt; The first prompt, the first analysis, the first ideas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February: Gemini writes the code.&lt;/strong&gt; The first custom widgets and functions came from Gemini. I also tried FlutterFlow's own page generator, as I told in episode 2. I dropped it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;March: Claude becomes the backbone.&lt;/strong&gt; Funny detail: the first time Claude appears in my notes, it's because another AI recommended it for writing texts (privacy policy, onboarding, notifications). By April it was reviewing the code. By June the main working prompt was written for Claude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-checks.&lt;/strong&gt; I kept asking the same question to other AIs. Gemini stayed my source of creative proposals: in spring it suggested QR codes, recurring events, an automatic check-out and an escape-room style game. Some of those ideas came back months later in a different shape. I also tried ChatGPT. For what I needed it was too agreeable and not concrete enough. A project like this needs someone who tells you when you're wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemini's ideas were often new and interesting. Claude had a better view of the whole project and stuck to the rules I had set. In the end I had one AI as the main developer and the others as reviewers and idea generators.&lt;/p&gt;

&lt;h2&gt;
  
  
  The handoff document
&lt;/h2&gt;

&lt;p&gt;The core of the method is a document I call the handoff: the state of the code, the decisions, the limits I discovered and a changelog. Every working session starts by loading it, together with a fresh export of the project.&lt;/p&gt;

&lt;p&gt;It grew with the app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Sections&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;June 1&lt;/td&gt;
&lt;td&gt;300 KB&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mid September&lt;/td&gt;
&lt;td&gt;976 KB&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;October 4&lt;/td&gt;
&lt;td&gt;721 KB&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At almost a megabyte I had to condense it. The document itself was becoming the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the AI connected to the project
&lt;/h2&gt;

&lt;p&gt;In May I connected the AI directly to the FlutterFlow project through MCP. It could read pages and apply small changes itself. The first batch went well: fourteen fixes applied that way. Then one write corrupted a custom code file. New rule, still valid: the AI can read the project and change settings, but custom code is pasted by me, in the editor, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worst AI mistake
&lt;/h2&gt;

&lt;p&gt;In the same weeks a chat produced a new version of the file with my push notification functions. It looked complete. The functions were there, with the right names. Their bodies were empty, with only a comment saying where the logic should go.&lt;/p&gt;

&lt;p&gt;Deploying it would have replaced working functions with functions that did nothing. Every notification in the app would have stopped without a single error. It was caught before the deploy. From that day a file that looks complete is not code for me until I've compared it with the one that's running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules against AI memory
&lt;/h2&gt;

&lt;p&gt;Most of the method is a list of rules, each with the date of the mistake that created it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;August 4:&lt;/strong&gt; never paste texts written by another AI into the project's knowledge. They contained behaviors of the app that didn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;August 17:&lt;/strong&gt; if I report the same symptom twice in a row, the AI's source is old. Stop diagnosing and ask for a fresh export.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;August 17:&lt;/strong&gt; a file is produced by editing the one in the export, never rewritten from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 7:&lt;/strong&gt; never use values "remembered" from previous sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 7:&lt;/strong&gt; when another AI makes a proposal, compare it point by point and say who wins, even when the winner isn't you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 25:&lt;/strong&gt; a number had been copied into six sections of the handoff and it was wrong. Every number now comes with the command that produces it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: an AI's memory sounds exactly as confident when it's wrong as when it's right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking five AIs the same question
&lt;/h2&gt;

&lt;p&gt;In September I had a product decision to make about which features to include. I gave the same prompt to five assistants: Gemini, ChatGPT, DeepSeek, Kimi and Perplexity. Then I had Claude compare the answers against the rules I had already set. Claude was judged too and lost a point for one of its own earlier proposals.&lt;/p&gt;

&lt;p&gt;The result surprised me: most of the proposals were things I had already decided. My note that day: when five assistants propose the same things you already decided, the list is complete, not poor. Asking more AIs didn't give me more ideas. It gave me confidence that I wasn't missing any.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;The method doesn't make me immune. I work on many fronts in parallel: the app, the backend, the website, the store pages, the outreach. Information still gets lost between them. A recent example: the invite page had been showing its countdown for weeks while my own facts document still listed it as missing.&lt;/p&gt;

&lt;p&gt;The handoff reduces these gaps. It doesn't remove them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use one AI as the main developer and the others as reviewers. Merging two developers' ideas by hand is a full-time job.&lt;/li&gt;
&lt;li&gt;An AI that agrees with everything is not helping. Look for the one that tells you no.&lt;/li&gt;
&lt;li&gt;Keep one document that every session starts from. Keep it a photo of the code, not a diary.&lt;/li&gt;
&lt;li&gt;Never deploy what an AI wrote without comparing it to what is running.&lt;/li&gt;
&lt;li&gt;Write a rule every time the AI's memory fails you, with the date. In a few months you'll have a method.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next and last episode: the finished app. What held, what I'd redo and why building it turned out to be the easy part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building orbiTome solo, with FlutterFlow and Firebase. More at &lt;a href="https://www.orbitome.com" rel="noopener noreferrer"&gt;orbitome.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>flutter</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Most of my bugs started with a word, not with code</title>
      <dc:creator>Cesare Bramante</dc:creator>
      <pubDate>Thu, 05 Nov 2026 11:38:52 +0000</pubDate>
      <link>https://dev.to/cbramante/most-of-my-bugs-started-with-a-word-not-with-code-51p5</link>
      <guid>https://dev.to/cbramante/most-of-my-bugs-started-with-a-word-not-with-code-51p5</guid>
      <description>&lt;p&gt;My app went public on the App Store and Google Play on July 13 and I've rebuilt it more times than I'd like to admit. When I look at the list of fixes, most of my bugs started with a word, not with code.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/cbramante/the-four-colors-from-my-first-prompt-are-still-in-my-app-almost-nothing-else-is-4980"&gt;last episode&lt;/a&gt; I told how the colors of my first prompt survived while five languages turned into four different places to keep in sync. This one is about how the app changed between the first sketch and today and about the fixes that taught me something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model that kept changing
&lt;/h2&gt;

&lt;p&gt;Some parts of the app changed shape three or four times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"I'd like to go".&lt;/strong&gt; In January it was one of three states of an event. In February the states became times: now, in one hour, tonight. In April "I'd like to go" was removed. In June it came back as its own feature, the Wish. Later it got its final name: "Vorrei", "I would like".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login.&lt;/strong&gt; Google, Apple and Microsoft in January. Google and Apple a week later. Then, in March, a phone number with a code, which fits an app built around the contacts you already have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The map.&lt;/strong&gt; In February it was a web map inside a WebView. Two weeks later it was a custom widget drawing OpenStreetMap tiles directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding.&lt;/strong&gt; Four pages with six animations in July. Two pages on September 1. One screen with a single "Create" button on September 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ads.&lt;/strong&gt; There was a small banner at launch. I removed it at the end of August because the benefit was close to zero. Removing it took more work than adding it because in my case Google Play kept blocking updates while an older build with ads was still active.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of August I rebuilt the whole interface. The screen to create an event went from 2,895 lines to 464. The code worked. People got lost: too many steps, too many choices, too much to read. To be honest it's the most expensive lesson of the whole project and I'll come back to it in the last episode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bugs that started with a word
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"In 1 hour" created the event now.&lt;/strong&gt; In May, in the Italian version, picking "In 1 hour" created an event starting immediately. The code decided the start time by checking the text of the button for English words like "now" or "1h". In Italian the button said "Tra 1 ora": nothing matched and the code fell back to "now". The fix was to map every visible label to a stable internal code first and compute from the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four months later, the same bug came back in Romanian.&lt;/strong&gt; In September a native review improved the Romanian translation: "Peste 1 oră" became "Peste o oră", which is more natural. Somewhere a piece of logic still depended on that label and the event started immediately again. The note I wrote that day: a translated label is a value, not a text. Translating it is a code change when something depends on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sentence in the app wasn't true.&lt;/strong&gt; One screen said the address book never leaves the phone. To match contacts, something derived from the numbers does leave it. Nobody had lied on purpose: the sentence had been written before the feature changed. I rewrote five texts that day and the lesson surprised me. Searching for words like "never" or "always" didn't find the false sentences. Only reading each one against what the code does found them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The website promised more than the app did.&lt;/strong&gt; The site said the invite "works even without the app". The invite page said "to reply, you need the app". Both were true. Read together they made a promise the app didn't keep. The sentence now is: anyone with the link sees the event; to reply, they install the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bugs that started somewhere else
&lt;/h2&gt;

&lt;p&gt;Not everything was about words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location check on future events.&lt;/strong&gt; Creating "Saturday at my place" while at home made the app think you had checked in. When you left the house, it closed the event, with all the replies. Found in September.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timer that read too much.&lt;/strong&gt; The home screen refreshed every 4 seconds and re-synced data at every tick, which by my estimate meant around 900 database reads per hour for each user. On a free tier that's the fastest way to a bill. Today it refreshes about every minute and a half. With a slow network it keeps showing the last good result instead of emptying the map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Tomorrow" that wasn't tomorrow.&lt;/strong&gt; In October, checking the code against daylight saving time, I found two places where "tomorrow" was computed as now plus 24 hours. On the day the clocks change a day lasts 23 or 25 hours, which can put the event on the wrong day. Now the date is built from the calendar parts (year, month, day plus one). The fix goes out with the next update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A space at the end of a name.&lt;/strong&gt; A settings collection created by hand in the console was called &lt;code&gt;app_config&lt;/code&gt; with a trailing space. The games didn't start and the error made no sense until I copied the name character by character.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A push that opened an empty page.&lt;/strong&gt; The notification carried the event's ID instead of its full path. Tapping it opened a blank event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some fixes I already described in my first article, the one about how events expire.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never put logic on a text the user can read. Decide it before the second language arrives.&lt;/li&gt;
&lt;li&gt;When you change a feature, reread every sentence that describes it, in the app and on the website, against what the code does now.&lt;/li&gt;
&lt;li&gt;The most expensive bug is a confusing screen. It doesn't crash. People just leave.&lt;/li&gt;
&lt;li&gt;Write down why you removed something. Removing is often harder than adding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next episode: working with several AIs and the rules I had to write because they all remember things wrong.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building orbiTome solo, with FlutterFlow and Firebase. More at &lt;a href="https://www.orbitome.com/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=vibecoding_ep4" rel="noopener noreferrer"&gt;orbitome.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>flutter</category>
      <category>debugging</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The four colors from my first prompt are still in my app. Almost nothing else is.</title>
      <dc:creator>Cesare Bramante</dc:creator>
      <pubDate>Thu, 29 Oct 2026 06:56:08 +0000</pubDate>
      <link>https://dev.to/cbramante/the-four-colors-from-my-first-prompt-are-still-in-my-app-almost-nothing-else-is-4980</link>
      <guid>https://dev.to/cbramante/the-four-colors-from-my-first-prompt-are-still-in-my-app-almost-nothing-else-is-4980</guid>
      <description>&lt;p&gt;The four brand colors I wrote in my first prompt in January are still in my app today, to the exact hex code. Almost nothing else from that first sketch survived: not the logo, not the font setup, and certainly not the way I handled five languages.&lt;/p&gt;

&lt;p&gt;This episode is about the visual identity and the language of the app, and about the AI's role in both. It's the part of the project where I disagreed with the AI the most. In &lt;a href="https://dev.toLINK_PUNTATA_2"&gt;the previous episode&lt;/a&gt; I told how one rule in the data model fixed the direction of the whole spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colors decided on day one
&lt;/h2&gt;

&lt;p&gt;My first prompt didn't just describe the app. It listed fourteen colors, each with a name and a job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary &lt;code&gt;#E040FB&lt;/code&gt;, a bright purple, for the main buttons;&lt;/li&gt;
&lt;li&gt;Secondary &lt;code&gt;#7C4DFF&lt;/code&gt;, an electric violet, for links and active states;&lt;/li&gt;
&lt;li&gt;Accent &lt;code&gt;#FF4081&lt;/code&gt;, neon pink, for notifications and social elements;&lt;/li&gt;
&lt;li&gt;Accent &lt;code&gt;#40C4FF&lt;/code&gt;, bright cyan, for status indicators;&lt;/li&gt;
&lt;li&gt;then backgrounds, text colors, success, warning and error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4okb32yr5a6sk4gn1b99.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4okb32yr5a6sk4gn1b99.png" alt="colors palette" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today the theme file of the app has the same four codes. It even keeps custom colors named after my January descriptions, translated into English: &lt;code&gt;primaryVibrantPurple&lt;/code&gt;, &lt;code&gt;secondaryElectricViolet&lt;/code&gt;, &lt;code&gt;accentNeonPink&lt;/code&gt;, &lt;code&gt;accentBrightCyan&lt;/code&gt;. Some things changed: a different red for errors, the dark backgrounds, and a lime green added later for public events.&lt;/p&gt;

&lt;p&gt;In August, during a redesign, the AI proposed a calmer look: one single accent color and warm neutrals. It applied it. I looked at it for a day and reverted it. Neon colors read better in both light and dark mode, and the neutral version looked flat. I wrote it down as a product decision not to reopen, because I knew the next session would propose it again.&lt;/p&gt;

&lt;p&gt;The AI was right about one thing, though. My brand purple on a white background has a contrast ratio of 3.2, below the 4.5 recommended for text. Links moved to a darker violet. The purple stayed for buttons and highlights, where it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  A font chosen for legibility
&lt;/h2&gt;

&lt;p&gt;The same January prompt chose the font: Atkinson Hyperlegible, a typeface designed to be easy to read for people with low vision. I wanted an app that anyone could read at a glance, outside, on a small screen.&lt;/p&gt;

&lt;p&gt;It's still there, but only for titles. In the August redesign the body text moved to Inter, which is denser and works better in lists and small labels.&lt;/p&gt;

&lt;p&gt;And here is the honest part. The app uses Atkinson and Inter. The website uses three other fonts. A printed flyer I made in August uses yet another family. Three type systems for one small product. The only thing they really share is the logo. If I started again, I would decide one type system for everything on day one, the same way I decided the colors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The logo I threw away
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2r8azbdfksxrrghooywm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2r8azbdfksxrrghooywm.png" alt="logo evolution" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first logo, from January 22, was three intertwined circles with a small lime star in the middle. Circles of friends overlapping. I liked it, and I used it in the first test builds.&lt;/p&gt;

&lt;p&gt;I dropped it after the very first builds for testers, before the public release, and redid it. The new one is a map pin with an orbit around it: a place, not a group of people.&lt;/p&gt;

&lt;p&gt;Looking back, the logo changed direction before I could say it in words. The app moved from "who" to "where" in the drawing first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five languages, the hardest part
&lt;/h2&gt;

&lt;p&gt;I wanted Italian, English, Spanish, French and Romanian from the first prompt, because I knew who I wanted to reach. It still turned out to be the hardest part of the identity, for reasons that had nothing to do with translation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI dropped a language.&lt;/strong&gt; On February 18 the master prompt listed the supported languages as "Italian, Spanish, French, German". Romanian had disappeared and German had appeared. The work plan of the next day set English as the main language and enabled Italian, Spanish and French. Nobody had asked for German. I only noticed because I reread the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first screens weren't translated at all.&lt;/strong&gt; In February the texts were written directly inside the widgets, in Italian: "Sono qui", "Mi piacerebbe". The spec then said every fixed string must go into FlutterFlow's language panel. That worked for labels, but not for everything: messages in snackbars, dialogs and custom code don't live in that panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So for a while I made a rule: whatever isn't in the panel stays in English&lt;/strong&gt;, as a common language. In July some screens showed Italian buttons next to English labels. I reversed the rule and translated everything. Today the app has 264 texts in five languages in the language panel, about 145 more translated field by field inside pages and components, and twelve custom code files that carry their own translation tables. The text of the permission requests lives in yet another place. "Five languages" turned out to mean four different places to keep in sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A language audit found the leaks.&lt;/strong&gt; An audit in August found sentences where one language had leaked into another: "Bienvenido par" (Spanish with French inside), "Pas besoin di rester" (French with Italian inside), and French that switched between formal and informal "you" across nineteen strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Words that changed
&lt;/h2&gt;

&lt;p&gt;Some of the best decisions were about words, not code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Italian&lt;/th&gt;
&lt;th&gt;English&lt;/th&gt;
&lt;th&gt;Spanish&lt;/th&gt;
&lt;th&gt;French&lt;/th&gt;
&lt;th&gt;Romanian&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wish&lt;/td&gt;
&lt;td&gt;Vorrei&lt;/td&gt;
&lt;td&gt;Wish&lt;/td&gt;
&lt;td&gt;Me gustaría&lt;/td&gt;
&lt;td&gt;Je voudrais&lt;/td&gt;
&lt;td&gt;Aș vrea&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghost mode&lt;/td&gt;
&lt;td&gt;Evento invisibile&lt;/td&gt;
&lt;td&gt;Invisible event&lt;/td&gt;
&lt;td&gt;Evento invisible&lt;/td&gt;
&lt;td&gt;Événement invisible&lt;/td&gt;
&lt;td&gt;Eveniment invizibil&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;I miei eventi&lt;/td&gt;
&lt;td&gt;My events&lt;/td&gt;
&lt;td&gt;Mis eventos&lt;/td&gt;
&lt;td&gt;Mes événements&lt;/td&gt;
&lt;td&gt;Evenimentele mele&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;Gruppi&lt;/td&gt;
&lt;td&gt;Groups&lt;/td&gt;
&lt;td&gt;Grupos&lt;/td&gt;
&lt;td&gt;Groupes&lt;/td&gt;
&lt;td&gt;Grupuri&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Wish&lt;/strong&gt; is a noun only in English. In the other four languages it became a verb in the conditional: "I would like". It sounds like something you say to a friend, not like a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ghost mode&lt;/strong&gt; and &lt;strong&gt;Vault&lt;/strong&gt; were good internal names. Users read "invisible event" and "my events".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags&lt;/strong&gt; became groups. Nobody tags their friends.&lt;/li&gt;
&lt;li&gt;The animated intro had a play on words, "orbit to me". I removed it: it doesn't survive translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide colors early and write down why. They are the part of a brand that lasts.&lt;/li&gt;
&lt;li&gt;Decide one type system for everything you'll ever publish, not only for the app.&lt;/li&gt;
&lt;li&gt;An AI will happily "improve" your identity every session. Write your decisions down as decisions, or you'll defend them again every week.&lt;/li&gt;
&lt;li&gt;Five languages is not a translation task. It's an architecture task: know where every string lives before you write the first one.&lt;/li&gt;
&lt;li&gt;Reread the AI's lists. It replaced a language and nobody would have noticed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next episode: evolutions and fixes. Most of my bugs started with a word, not with code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building orbiTome solo, with FlutterFlow and Firebase. More at &lt;a href="https://www.orbitome.com" rel="noopener noreferrer"&gt;orbitome.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>design</category>
      <category>i18n</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Every revision made my app spec better. The fix was one rule about data.</title>
      <dc:creator>Cesare Bramante</dc:creator>
      <pubDate>Wed, 14 Oct 2026 04:28:03 +0000</pubDate>
      <link>https://dev.to/cbramante/every-revision-made-my-app-spec-better-the-fix-was-one-rule-about-data-223g</link>
      <guid>https://dev.to/cbramante/every-revision-made-my-app-spec-better-the-fix-was-one-rule-about-data-223g</guid>
      <description>&lt;p&gt;I shipped a native app on both stores, in five languages, on my own, with a general-purpose AI as my developer. Between my first prompt and the first screen that did what I wanted, there were six versions of a "master prompt". This episode is about those versions, and about why the fix I needed wasn't in any of them.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/cbramante/i-gave-an-ai-my-first-app-spec-it-cut-half-of-it-and-took-the-other-half-too-literally-31i6"&gt;the first episode&lt;/a&gt; I told how the AI took my first line literally: I wrote "share your position", and it gave me a map of friends. This is what happened next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The master prompt
&lt;/h2&gt;

&lt;p&gt;Every chat with an AI starts from zero. So from the first week I kept one document that I pasted at the start of every new session: what the app is, who it's for, how it works, which decisions are already made. I called it the master prompt.&lt;/p&gt;

&lt;p&gt;It changed nature faster than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;January 26:&lt;/strong&gt; vision, user flow, colors. Basically my first prompt plus the AI's answer, cleaned up. One line in capitals that I still like: "remove the word 'invite' completely".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February 13:&lt;/strong&gt; a technical spec. Tables, pages, triggers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February 18:&lt;/strong&gt; the same spec rewritten in FlutterFlow's own vocabulary: page state, actions, conditional visibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same day, seven minutes apart:&lt;/strong&gt; a new version "synced with what I actually built in the editor".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one was the real change. The master prompt stopped being a description of what I wanted and became a photo of what existed. An AI that reads a wish will build the wish. An AI that reads a photo will start from where you are.&lt;/p&gt;

&lt;p&gt;By spring I had two branches of the document, numbered in a way that makes no sense today (a v4.0 in March, then a v3.2 and v3.3 in April and June). More on that in episode 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec that ran every ten seconds
&lt;/h2&gt;

&lt;p&gt;In late January the AI generated two files for me and called them FlutterFlow exports. The simple one had exactly the five screens it had recommended. The "advanced" one had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"update_live_location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"runs every 10 seconds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"share_location_event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"writes to locationShares"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"join_event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"adds user to events.participants"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus friends' pins on the map, a live location field on every user, and a collection to share your position with an expiry.&lt;/p&gt;

&lt;p&gt;Three problems, in increasing order of importance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Battery.&lt;/strong&gt; A position update every ten seconds is the fastest way to get uninstalled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stores.&lt;/strong&gt; Both stores are strict about location in the background as a main feature. The app would have had a hard time getting approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was the app I didn't want.&lt;/strong&gt; People on the map, not events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And a fourth one, smaller and funnier: the file couldn't be imported in my version of FlutterFlow, and in the same configuration it mixed a Mapbox style address into a MapLibre map. A spec that looked perfectly plausible and couldn't run anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  I also tried FlutterFlow's own AI
&lt;/h2&gt;

&lt;p&gt;In February I tried FlutterFlow's built-in page generator for the first screens. One of my prompts started with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a map UI like Zenly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Zenly was the best-known app for seeing your friends on a map. So I was asking a second AI to build, again, the version of my idea I didn't want. The generator was quick for empty layouts. I didn't like what it gave me, so I built the pages myself in the editor. The logic code came from the general-purpose AI, and FlutterFlow compiled it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was one line about data
&lt;/h2&gt;

&lt;p&gt;Rewriting the pitch didn't change the direction. Every new answer still leaned on "position". What changed it was a rule in the data model.&lt;/p&gt;

&lt;p&gt;On February 13 a line appeared in the spec: no position saved outside an active event. On February 18 the users table no longer had any location field at all. A position existed only as the place of an event, and the event had an expiry date. The cloud database became, in the words of that spec, a bulletin board for pins on a map.&lt;/p&gt;

&lt;p&gt;After that, the AI could not drift back even if it wanted to. There was simply nowhere to store where a person is. You can argue with an AI about intent for weeks. A missing field ends the argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The revision loop that came later
&lt;/h2&gt;

&lt;p&gt;The real cycles of revision started in April, with a working app and a second AI reviewing the code. In one week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;April 22: first audit;&lt;/li&gt;
&lt;li&gt;April 23: nineteen fixes;&lt;/li&gt;
&lt;li&gt;April 24: second audit, 23 problems;&lt;/li&gt;
&lt;li&gt;April 26-27: final audit before release, 31 bugs;&lt;/li&gt;
&lt;li&gt;April 30: verification. Eleven of the thirteen "pending" fixes were already in the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documentation was behind the code, not the other way around. From then on the rule was: when the code and the document disagree, the code wins. And one more, learned the hard way: a fix declared "applied" must be checked against the previous export, because one of them had introduced a new bug.&lt;/p&gt;

&lt;p&gt;To keep the document up to date I ended up with a very short prompt, which I still use (translated from Italian):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scan &amp;lt;chat&amp;gt; and &amp;lt;audit&amp;gt;, extract every fix and decision.
Output: ONLY a diff 🟢/🟡/🔴 per section. No preamble. Nothing unchanged.
Priority: audit &amp;gt; chat &amp;gt; document.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Revisions improve the form of a spec. They rarely fix its intent.&lt;/li&gt;
&lt;li&gt;If your idea goes against the common version, put it in the data model, not only in the description. A rule on data is the one thing the AI can't reinterpret.&lt;/li&gt;
&lt;li&gt;Keep your main prompt as a photo of what exists, not a list of what you want.&lt;/li&gt;
&lt;li&gt;When the code and the document disagree, believe the code. Then fix the document.&lt;/li&gt;
&lt;li&gt;A generated spec has to be read line by line. "Every 10 seconds" was right there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next episode: the first sketch. Colors, a font chosen on day one, a logo I threw away, and five languages that turned out to be the hardest part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building orbiTome solo, with FlutterFlow and Firebase. More at &lt;a href="https://www.orbitome.com" rel="noopener noreferrer"&gt;orbitome.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>flutter</category>
      <category>firebase</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I gave an AI my first app spec. It cut half of it and took the other half too literally</title>
      <dc:creator>Cesare Bramante</dc:creator>
      <pubDate>Thu, 08 Oct 2026 07:58:06 +0000</pubDate>
      <link>https://dev.to/cbramante/i-gave-an-ai-my-first-app-spec-it-cut-half-of-it-and-took-the-other-half-too-literally-31i6</link>
      <guid>https://dev.to/cbramante/i-gave-an-ai-my-first-app-spec-it-cut-half-of-it-and-took-the-other-half-too-literally-31i6</guid>
      <description>&lt;p&gt;A native app on both stores, in five languages, built by one person with an AI as the developer. This is how it started: the day an AI took my first spec apart.&lt;/p&gt;

&lt;p&gt;"Computers are good at following instructions, but not at reading your mind." Donald Knuth, 1984&lt;/p&gt;

&lt;p&gt;Forty years later, it turns out this is still true when the computer is an AI.&lt;/p&gt;

&lt;p&gt;I'm a solo developer. I didn't write the app the classic way: a general-purpose AI was my developer, and FlutterFlow was my compiler. This series is how that went, from the first prompt to the stores, including the parts that didn't work.&lt;/p&gt;

&lt;p&gt;This first episode is about the idea, and about what the AI did to it.&lt;/p&gt;

&lt;p&gt;Who I am, briefly&lt;/p&gt;

&lt;p&gt;I'm 56 and I come from IT. I started as a computer technician, then worked as an IT security consultant, then spent years building web portals. For a while I also taught at university as a subject expert, without a degree. So I'm not new to software, but I had never built a mobile app. I work alone, and the app is meant for people a lot younger than me.&lt;/p&gt;

&lt;p&gt;Where the idea came from&lt;/p&gt;

&lt;p&gt;Having kids, and looking around me, I had the feeling that people see each other less. Not because they don't want to, but because organizing anything takes a group chat, fifty messages and a decision nobody wants to make.&lt;/p&gt;

&lt;p&gt;And most social apps push the other way. They are built to keep you on the phone, and they reward showing a life other people can't reach. I wanted an app you open in order to put the phone down: you see where something is happening, you go.&lt;/p&gt;

&lt;p&gt;I looked for an app that did this. Some had been born, had an early success and then closed. Others were very niche: only parties, only sports, only public events. Nothing for the simple case: a few friends who want to meet tonight.&lt;/p&gt;

&lt;p&gt;So I installed a bunch of these apps, used them for a while, and in January I wrote my first prompt, to Gemini.&lt;/p&gt;

&lt;p&gt;The first prompt&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fovwbph7627t6r0ilq3lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fovwbph7627t6r0ilq3lr.png" alt="Italian text of the original app spec, starting with the goal: sharing your current and future position with friends and groups" width="799" height="367"&gt;&lt;/a&gt; My original prompt to Gemini, January 2026 (in Italian). The first line says "share your current and future position".&lt;/p&gt;

&lt;p&gt;It was a full spec. Contacts imported from the phone and turned into tags. Three states for every event: "I'm here", "I'll be there", "I'd like to go". Email invites for people without the app. A cloud backup. Five languages to pick from at install. A color palette, a font, even the ad banner position.&lt;/p&gt;

&lt;p&gt;Reading it today, it's a wish list. And the first line has a problem I didn't see at the time.&lt;/p&gt;

&lt;p&gt;What the AI told me&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlp9lcq5zdmdnv8t5fnt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlp9lcq5zdmdnv8t5fnt.png" alt="Italian text of the AI's answer: the idea is clear but too complex, with the golden rule " width="800" height="410"&gt;&lt;/a&gt; The beginning of the AI's answer (in Italian). The bold line at the end is the rule I still use.&lt;/p&gt;

&lt;p&gt;The answer started kindly and then went straight to the point: the idea was clear, but too complex for the value people would perceive. The user had to understand too much before getting anything back. And then one sentence that I still use as a test for every new feature:&lt;/p&gt;

&lt;p&gt;In 30 seconds I must be able to say "I'm here" and invite someone.&lt;/p&gt;

&lt;p&gt;Then it went through everything. Onboarding in three steps at most. The map as the home screen, not as a feature. Tags that look like groups with an emoji, not like a database field. Invites through a link you can paste in any chat, with email only as a fallback. Cut the manual backup, cut the advanced history, use the system language.&lt;/p&gt;

&lt;p&gt;It also took my first line at face value. I had written "share your position", and it built on that: friends' avatars visible on the map, a "social map" where you see who is going where, a live "I'm here" position with a two hour timer. To be fair, that is exactly what my words said. It wasn't what I meant.&lt;/p&gt;

&lt;p&gt;What survived, and what didn't&lt;/p&gt;

&lt;p&gt;Looking at the app that is on the stores today:&lt;/p&gt;

&lt;p&gt;Kept: the map as the home screen, with different pins for different kinds of plans. The invite link, which became the most important feature: whoever opens it sees the place, the time and who's coming, even without installing anything. Tags became groups, and you can filter the map by group. Every event got a QR code. A ghost mode, although today it means something else: the event is visible only to the people you invited. And "I'd like to go", which became the Wish: you post an idea and see who's in before organizing anything.&lt;br&gt;
Kept, but moved: the idea of a timer. It's no longer on my position, it's on the event: an event leaves the cloud shortly after it ends.&lt;br&gt;
Refused: the advanced history cut. I kept the history, but it stays on the phone, not in the cloud.&lt;br&gt;
Dropped, further than the AI suggested: email invites. The AI wanted email as a fallback; I removed it completely, because people get too many emails and ignore most of them. I looked at SMS too, but it felt archaic and every message costs money. And sending invites through messaging apps meant the answers would come back scattered across different chats, with no way to collect them. A link solved both problems: it travels in any chat, and you see who's coming in the event, not in a thread.&lt;br&gt;
Dropped: the viral mechanics. Some were mine (invite three friends to unlock premium), some were the AI's (badges).&lt;br&gt;
Refused outright: people on the map. It was the one thing I was sure about.&lt;br&gt;
Ignored, then came back to: languages. I wanted five, and I kept five. But at first I only filled in FlutterFlow's language panel, and it turned out to be a dead end: snackbars and alerts weren't translated. Today the app follows the localization of the device, with English as a fallback for the few strings I missed. Not far from what the AI had suggested on day one.&lt;br&gt;
The part I only understood later&lt;/p&gt;

&lt;p&gt;The cuts were the easy part. The real lesson was in the first line.&lt;/p&gt;

&lt;p&gt;"Share your position" is what location sharing apps do, so the AI gave me the most common version of that idea, and every following answer built on it. What I wanted was almost the opposite: show where things happen, not where people are. The AI read my words, not my intent. It took me months, and a lot of rewriting, to make the spec say what I meant.&lt;/p&gt;

&lt;p&gt;What I took from it&lt;br&gt;
A first spec is a wish list. Writing it is useful; building it as it is, isn't.&lt;br&gt;
AI is very good at cutting. It has no attachment to your features.&lt;br&gt;
AI fills every ambiguity with the most common version of your idea. If your idea goes against the common version, say it in the first line.&lt;br&gt;
The 30-second rule works for every screen, not only for onboarding.&lt;br&gt;
Some of its cuts were right, some I refused, some I had to undo months later. It didn't know my users, and it couldn't.&lt;/p&gt;

&lt;p&gt;Next episode: the revision loop, and the day a generated spec put my friends' live position on the map every ten seconds.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I made events (and their data) expire in a FlutterFlow + Firebase app</title>
      <dc:creator>Cesare Bramante</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:37:22 +0000</pubDate>
      <link>https://dev.to/cbramante/how-i-made-events-and-their-data-expire-in-a-flutterflow-firebase-app-16h9</link>
      <guid>https://dev.to/cbramante/how-i-made-events-and-their-data-expire-in-a-flutterflow-firebase-app-16h9</guid>
      <description>&lt;p&gt;Most apps are built to remember. I wanted to build one that forgets on purpose: as little as possible left in the cloud, and what's worth remembering kept only on the phone of the person who lived it.&lt;/p&gt;

&lt;p&gt;I'm an indie developer, and I build orbiTome. You drop an emoji pin on a map, pick a time, invite people from your contacts, and meet up. It's built with FlutterFlow and Firebase, and it's live on iOS and Android in five languages.&lt;/p&gt;

&lt;p&gt;One design rule shaped most of the backend: &lt;strong&gt;an event should leave the cloud shortly after it ends.&lt;/strong&gt; Plans with friends usually live in group chats, and group chats keep everything forever: where you were, when, with whom. I wanted the opposite. The map shows events, not people. When the event is over, its cloud document is deleted together with the guests' replies. Your own history (what you created, what you joined, the photos you took) stays on your phone.&lt;/p&gt;

&lt;p&gt;This post covers how that works in practice, and what I'd tell anyone trying the same thing in FlutterFlow.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Expires" is not one number
&lt;/h2&gt;

&lt;p&gt;My first version had one rule: delete the event two hours after it starts. It didn't survive, because events aren't all alike.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When you create it&lt;/th&gt;
&lt;th&gt;Starts&lt;/th&gt;
&lt;th&gt;Expires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Now / In 1 hour / In 2 hours&lt;/td&gt;
&lt;td&gt;the chosen time&lt;/td&gt;
&lt;td&gt;start + 2 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tonight&lt;/td&gt;
&lt;td&gt;21:00 (if that's past, in 3 hours)&lt;/td&gt;
&lt;td&gt;23:59 the same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tomorrow night&lt;/td&gt;
&lt;td&gt;21:00 tomorrow&lt;/td&gt;
&lt;td&gt;23:59 tomorrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pick a date&lt;/td&gt;
&lt;td&gt;chosen date and time&lt;/td&gt;
&lt;td&gt;start + 4 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wish with a time ("anyone up for a hike Saturday at 9?")&lt;/td&gt;
&lt;td&gt;the proposed time&lt;/td&gt;
&lt;td&gt;at the proposed time, no buffer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wish, no time set&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;7 days after posting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public event from an organization&lt;/td&gt;
&lt;td&gt;set by the source&lt;/td&gt;
&lt;td&gt;end set by the source, or start + 4 h; never more than 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of the table, two rules apply to every event created in the app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ceiling:&lt;/strong&gt; no event lives more than 6 hours past its start. (A Wish with no date has no start, so it isn't affected.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Floor:&lt;/strong&gt; no expiry falls less than 2 hours from now. A Wish set for 30 minutes from now still lives two hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expiry in the table is a default, not a promise. While the event is running, the app checks the organizer's position every time the app is open on their phone, never in the background. If the organizer is within 50 meters of the pin and the expiry is less than 30 minutes away, the expiry moves to one hour from now, as many times as needed: a meetup that runs long doesn't vanish halfway through. The 6-hour ceiling still applies. It also works the other way: if the organizer checked in and then moves more than 150 meters away, the event ends there, or after a 30-minute grace period if someone else is still on site. Wishes are never extended or closed this way: they're intentions, not places.&lt;/p&gt;

&lt;p&gt;A plan for the evening that starts at 21:00 shouldn't vanish at 23:00 while people are still out. An unanswered Wish, on the other hand, is a loose proposal: it has no reason to exist past the moment it was about. If someone replies and the creator confirms, the Wish becomes a real event and its expiry is recalculated: start + 2 h.&lt;/p&gt;

&lt;p&gt;I didn't design the ceiling and the floor on a whiteboard. Each one came from a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The floor.&lt;/strong&gt; A Wish created with "Now" had its expiry equal to its start, which was now. It was born expired and disappeared from the map without any error. When I tightened the security rules, the same creation started getting rejected. An expiry that isn't in the future is always a mistake, so now there's a two-hour minimum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ceiling.&lt;/strong&gt; "Tonight" picked after 21:00 starts the event three hours later, after midnight, and "23:59 of the start day" became 23:59 of the following night: an event starting at 1:30 stayed alive for 22 and a half hours. The 6-hour ceiling closes that case, and other parts of the app (check-in, photos) can rely on it as a rule instead of an assumption.&lt;/p&gt;

&lt;p&gt;A third bug was about languages. The code recognized "Tonight" by comparing the button's label, and in French, Spanish and Romanian it never matched: in those languages every "Tonight" expired after 2 hours. Now the visible label, in any language, is first mapped to a stable code (&lt;code&gt;tonight&lt;/code&gt;, &lt;code&gt;tomorrow&lt;/code&gt;, &lt;code&gt;pick&lt;/code&gt;…), and expiry is computed from the code. With five languages, no logic should depend on a string the user can read.&lt;/p&gt;

&lt;p&gt;The lesson: decide expiry &lt;strong&gt;per type of plan&lt;/strong&gt;, not per database. It also changed how I write the app's copy. I no longer say something "disappears after 2 hours", because it isn't true for every event. What I can say is that an event leaves the cloud shortly after it ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Where the expiry is computed
&lt;/h2&gt;

&lt;p&gt;The expiry timestamp is computed &lt;strong&gt;when the event is created&lt;/strong&gt;, by a small FlutterFlow Custom Function, and stored in the event document as &lt;code&gt;expires_at&lt;/code&gt;. After creation, only two other places change it: turning a Wish into an event (start + 2 h) and the on-site check while the organizer has the app open (extend or close). All three respect the same 6-hour ceiling; everything else only reads the field.&lt;/p&gt;

&lt;p&gt;Computing it at creation keeps everything else simple. The map queries, the cleanup and the invite page all read one field, and none of them re-implements the rules.&lt;/p&gt;

&lt;p&gt;The trade-off is that the client computes the value. The security rules don't recompute it, but they constrain it: on create, &lt;code&gt;expires_at&lt;/code&gt; must exist, must be a timestamp, must be in the future and no more than 400 days out, and the same cap applies on update. The 6-hour ceiling lives only in the client, so a tampered client could keep its own event online for up to 400 days. It's a known limit, and I accepted it because it only affects the person doing it.&lt;/p&gt;

&lt;p&gt;The "must be in the future" check wasn't there from day one, for a specific reason: security rules also apply to the old app versions still installed on people's phones. I only turned it on once the version with the 2-hour floor was effectively the only one in use. Before that, it would have rejected events created by older versions.&lt;/p&gt;

&lt;p&gt;There's one exception. Some events are public and come from city bodies, non-profits and public institutions. They are never created from the app: they come in only through a dedicated web portal, via a private Cloud Function that writes with admin privileges and sets expiry server-side (the end given by the source, or start + 4 h, and never more than 30 days). The security rules reject any client write that tries to create an event in that category or convert an existing one. In short, the client decides expiry only for its own private events.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Who actually deletes the data
&lt;/h2&gt;

&lt;p&gt;Two mechanisms, with two different jobs.&lt;/p&gt;

&lt;p&gt;The first is a &lt;strong&gt;scheduled Cloud Function&lt;/strong&gt; that runs every 60 minutes: it looks for events whose &lt;code&gt;expires_at&lt;/code&gt; has passed and deletes them in batches. In practice an event leaves the cloud within about an hour of expiring.&lt;/p&gt;

&lt;p&gt;The second is a &lt;strong&gt;Firestore TTL policy&lt;/strong&gt; on the same field, as a safety net: if the function misses a run, Firestore deletes the documents anyway. On its own it wasn't enough for me. Google's docs say expired data is "typically deleted within 24 hours", and until then expired documents still show up in queries.&lt;/p&gt;

&lt;p&gt;That's why every query that shows events filters out those whose &lt;code&gt;expires_at&lt;/code&gt; has passed, and the client drops them again before drawing them. &lt;strong&gt;"Expired" and "deleted" are two different states:&lt;/strong&gt; the UI hides an event the moment it expires, even if the backend removes it later.&lt;/p&gt;

&lt;p&gt;What goes away with the event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guests' replies and scheduled reminders.&lt;/strong&gt; A function triggered on the document's deletion removes them, whatever the cause: the creator deleting the event, the hourly cleanup or TTL. One cleanup point instead of three. Before it existed, replies to deleted events stayed in the database forever, because the security rules (rightly) don't let the creator delete other people's replies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guest list&lt;/strong&gt;, which is a field of the document itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subcollections&lt;/strong&gt; (for example game data inside an event). Firestore doesn't delete them with the parent document, neither on a normal delete nor through TTL. So each of their documents carries its own expiry, slightly after the event's, with its own TTL policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files in the cloud:&lt;/strong&gt; none to delete. Event photos and videos are never uploaded (see section 4). Firebase Storage only holds profile pictures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule I took from this: for every piece of data born around an event, ask who will delete it, and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keeping history without keeping it in the cloud
&lt;/h2&gt;

&lt;p&gt;Events leave the cloud, but people still want to remember them. So the events that involve you are copied into a local SQLite database on your phone, which I call the Vault.&lt;/p&gt;

&lt;p&gt;For each event the Vault keeps the title, emoji, date and time, place name and coordinates, description, organizer's name, your role and your status ("I'm here now", "I was there", "Cancelled by the organizer"). It doesn't keep the list of participants.&lt;/p&gt;

&lt;p&gt;The copy is created right away, not at expiry, because by then the cloud document may already be gone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when you create an event or a Wish;&lt;/li&gt;
&lt;li&gt;when you reply "I'll be there";&lt;/li&gt;
&lt;li&gt;when you get an invite, but as a provisional copy: if you don't reply, it goes away after the event starts, unless you checked in or added photos. If the organizer cancels before the start, it stays marked "Cancelled by the organizer" for 7 days;&lt;/li&gt;
&lt;li&gt;when you open the app on site, check-in moves your status to "I'm here now" and then to "I was there".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why not Firestore's offline cache? Because the cache mirrors the server: when the event is deleted in the cloud, it disappears from the cache at the next sync. And the cache isn't an archive anyway, Firestore prunes it on its own. I needed the opposite: a copy that outlives the server document.&lt;/p&gt;

&lt;p&gt;The Vault can leave the phone only as an encrypted backup that the user saves wherever they want.&lt;/p&gt;

&lt;p&gt;Event photos and videos follow the same logic. They go into the app's private folder, not the gallery. Before saving, metadata is stripped, location included, and if it can't be stripped the file isn't saved. They're never uploaded, nobody else sees them, and they stay as long as the event stays in your Vault: delete it, or sign out, and they go with it. The encrypted backup doesn't include them.&lt;/p&gt;

&lt;p&gt;Three FlutterFlow + SQLite lessons that cost me hours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In SQLite queries defined in the FlutterFlow panel, parameters must be written as &lt;code&gt;${name}&lt;/code&gt;.&lt;/strong&gt; FlutterFlow doesn't pass arguments to the query, so &lt;code&gt;?&lt;/code&gt; placeholders stay empty and the &lt;code&gt;WHERE&lt;/code&gt; filters nothing: no error, just wrong results. Careful, though: &lt;code&gt;${name}&lt;/code&gt; inserts the text as-is, it's not a real bound parameter. An apostrophe in a search term breaks the query, so escape it. In Custom Action Dart code, &lt;code&gt;?&lt;/code&gt; with an argument list works normally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations.&lt;/strong&gt; &lt;code&gt;CREATE TABLE IF NOT EXISTS&lt;/code&gt; doesn't change a table that already exists, and the database bundled with the app only applies to new installs. Users who update need &lt;code&gt;ALTER TABLE&lt;/code&gt; with nullable or defaulted columns, a check that the column doesn't already exist before adding it, and a schema version (I use &lt;code&gt;PRAGMA user_version&lt;/code&gt;) that only moves forward when the step succeeded. Users update from different versions, and a restored backup can carry an older schema than the installed app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Panel queries are for reading.&lt;/strong&gt; FlutterFlow runs the SQLite queries from the panel with &lt;code&gt;rawQuery&lt;/code&gt;, which is meant for reads: an &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; written there fails silently. Writes belong in a Custom Action, with &lt;code&gt;rawInsert&lt;/code&gt;, &lt;code&gt;rawUpdate&lt;/code&gt; or &lt;code&gt;rawDelete&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An unplanned side effect: reading history from SQLite instead of Firestore takes the Vault's reads to zero. That matters on the free tier, and it matters more as users grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What happens to the invite link
&lt;/h2&gt;

&lt;p&gt;Every event has a link that works without the app: whoever opens it sees the emoji, title, organizer, place, time with a live countdown, and how many people confirmed. The page is served by a Cloud Function that asks &lt;code&gt;getJoinPreview&lt;/code&gt; for the data (an endpoint that returns a closed list of fields, one read per open) and rewrites the Open Graph meta tags. WhatsApp, Telegram and iMessage don't run JavaScript, so without that step link previews would only show generic text.&lt;/p&gt;

&lt;p&gt;Anyone with the link sees the preview, even without an account, so the link should only go to the people you want. Inside the app, the event stays visible only to the organizer's contacts or, in Ghost mode, only to the invited guests.&lt;/p&gt;

&lt;p&gt;After expiry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in the app, opening the link of an expired event shows "This event has ended." and a "Propose again" button that starts a new event from it;&lt;/li&gt;
&lt;li&gt;on the web, between expiry and deletion (about an hour at most), new previews say "This event has ended." instead of organizer and place; the title with the date stays;&lt;/li&gt;
&lt;li&gt;after deletion, &lt;code&gt;getJoinPreview&lt;/code&gt; answers that the event doesn't exist and the link shows orbiTome's generic invite page, with no event data. A CDN-cached copy can survive two minutes at most.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One last thing deserves a check in any app like this: a link preview already pasted into a group chat keeps showing title, date, organizer and place, because the chat stored the preview, not your server. You can't take back what's already been shared. You can only stop serving it, and decide from the start what goes into the preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What I'd tell someone starting today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide expiry per type of plan, write your copy to match, and don't promise a fixed number of hours.&lt;/li&gt;
&lt;li&gt;Store one expiry timestamp at creation, with a ceiling and a floor. Keep the places that can change it few, make them all respect the same ceiling, and let everything else just read it.&lt;/li&gt;
&lt;li&gt;Compute on codes, not on translated labels.&lt;/li&gt;
&lt;li&gt;Hide data on expiry, delete it later, and filter in every query. Firestore TTL is a great safety net, but if latency matters, pair it with a scheduled cleanup.&lt;/li&gt;
&lt;li&gt;For every piece of data born around the event (replies, reminders, subcollections), decide who deletes it.&lt;/li&gt;
&lt;li&gt;Tighten security rules only when the client version that respects them is the one almost everyone runs.&lt;/li&gt;
&lt;li&gt;If history matters, copy it to the device when it's born, not when it's about to disappear.&lt;/li&gt;
&lt;li&gt;Check what shared links show once the data is gone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building an app that forgets turned out to be harder than building one that remembers. But it changed how I explain the product: "the event ends, and so does its data" is something people get in one sentence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building orbiTome solo, with FlutterFlow + Firebase. If you're working on something similar, I'd love to compare notes in the comments. More at &lt;a href="https://www.orbitome.com" rel="noopener noreferrer"&gt;orbitome.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>mobile</category>
      <category>database</category>
    </item>
  </channel>
</rss>
