# Nerdy Production > Expert software development company specializing in Flutter mobile apps, web applications, and AI-powered services. Based in Europe, serving clients worldwide. ## Add-to-App: Flutter in Your Native App https://nerdy.pro/services/flutter-app-development/add-to-app Add-to-app is Flutter's official mechanism for embedding a Flutter module inside an existing native iOS or Android application. The host app stays exactly what it is — your Swift or Kotlin codebase, your release process, your users — and Flutter arrives as one compiled module behind it, rendering the screens you choose to give it. Each of those screens is written once and runs on both platforms. This page is for teams who want Flutter *inside* the app they already have: a native product nobody is going to rewrite, whose next features keep costing twice because they must be built for iOS and Android separately. If your actual goal is to end up on Flutter entirely, that is a different engagement with the same mechanics — read [React Native and native migration](https://nerdy.pro/services/flutter-app-development/flutter-migration) instead, which is honest about being a rewrite. Here, nothing gets rewritten: the native app remains the product, and Flutter earns its place one feature at a time. #### The engagements we run - **A pilot feature** — one bounded feature built in Flutter and shipped inside both of your native apps, with the integration cost and the payoff measured rather than argued about - **Feature-parity catch-up** — the screen your iOS app has and your Android app doesn't (or the other way round), built once for both instead of a second time for one - **The new surface** — a loyalty program, a booking flow, a chat: a self-contained addition to a mature native product, reaching both platforms on one budget - **Adoption groundwork** — the module scaffolding, router integration, CI wiring, and platform-channel contracts set up properly, so your own engineers ship the features ### Who This Is For — and Who Should Migrate Instead Add-to-app pays for itself under specific conditions, and we would rather name them than sell an integration to a team that needs something else. #### Signs add-to-app fits - Every roadmap feature is built twice, and the iOS and Android versions of your app have started to drift apart in capability - The app is too large or too proven to rewrite, so "just use Flutter" has never been an available answer - You want to evaluate Flutter on real production work — one feature, real users, measured — before making any larger commitment - Two platform teams are hard to staff evenly, and new surfaces stall on whichever platform is short-handed this quarter #### When it is NOT the right call - The app is small enough that a fresh Flutter build or a screen-by-screen [migration](https://nerdy.pro/services/flutter-app-development/flutter-migration) costs less than maintaining a native–Flutter seam — integration has overhead, and a small host cannot amortize it - The decision to end up 100% Flutter is already made — then run it as a migration from day one, with a migration map, rather than drifting into one - The app is already React Native — embedding a second cross-platform runtime next to the first one compounds the problem; that team's realistic options are staying put or migrating - The feature you want is a deep platform capability — a widget-led experience, Watch-first functionality — where Flutter is the wrong tool and we will say so > **The honest read:** a hybrid app is a permanent seam — two toolchains, two sets of idioms, and a boundary that every engineer on the team has to understand. Add-to-app is worth that cost when the host app is too big to rewrite and the roadmap keeps paying the two-platform tax. If neither is true of your app, one of the neighbouring pages is the better answer, and the first call is where we tell you which. ### How Incremental Adoption Runs #### The Add-to-App Flow 1. **Native App Stays the Product** Your iOS and Android apps keep shipping unchanged — no freeze, no parallel rewrite, no cutover on the horizon 2. **Flutter Module Embedded** One Flutter module is wired into both hosts, sharing their navigation, authentication, and analytics through defined contracts 3. **Pilot Feature Ships** A bounded feature is built once in Flutter and released inside both apps, with size and startup cost measured before and after 4. **Adoption Grows Feature by Feature** Each new surface that lands in the module is one you did not build twice — and each one is a decision, not an obligation 5. **You Decide the Endgame** Stay hybrid indefinitely, hand the module to your own team, or convert the momentum into a full migration — every boundary is a stopping point The flow is deliberately the same machinery our [migration service](https://nerdy.pro/services/flutter-app-development/flutter-migration) uses — a Flutter module behind a shared router, screens moving one at a time. The difference is the destination: a migration retires the native host at the end, and adoption never has to. That also means adoption converts cleanly into a migration later if the module earns it, with nothing thrown away. ### What Add-to-App Actually Demands #### One engine, with an owned lifecycle A Flutter module brings the Flutter engine with it, and the engine is a resource the host app must manage deliberately: pre-warmed so the first Flutter screen opens without a visible spin-up, shared across entry points rather than instantiated per screen, and released when the platform asks for memory back. Getting this wrong is invisible in a demo and obvious in production. It is the first thing we build, not the last thing we tune. #### Navigation across the boundary Users do not care which framework rendered the screen they are on, and the navigation must agree: back gestures behaving per platform convention, deep links landing on Flutter screens as reliably as native ones, and state surviving the crossing in both directions. The router contract between host and module is the piece of add-to-app architecture that decides whether the seam is invisible or a permanent source of bugs. #### Shared services, borrowed rather than duplicated Your app already has authentication, analytics, networking, and feature flags. The Flutter module must borrow them across platform channels — not grow its own copies, which is how a hybrid app ends up with two sessions, two event schemas, and metrics nobody trusts. Defining those channel contracts precisely is most of the groundwork engagement, and it is work that pays out on every feature that follows. #### Two toolchains in one pipeline After integration, your CI/CD builds the Flutter module and both native hosts, and the module is versioned against two release trains that will not always move together. We wire the module build into your existing pipeline — cached, reproducible, and owned by your repo rather than by a laptop — because an integration your CI cannot build is not integrated. #### A seam your designers can live with Flutter draws its own pixels, which cuts both ways: it will not inherit your native components, but it will reproduce your design system exactly — once your tokens, typography, and spacing are ported into the module's theme. We do that porting as part of the groundwork, so a user scrolling from a native screen into a Flutter one has no way to tell where the boundary was. #### The costs, measured instead of asserted Embedding Flutter adds real weight: on the order of several megabytes to an Android release and somewhat more on iOS, plus engine startup on first launch of a Flutter screen. The exact numbers depend on your app, which is why the pilot's deliverables include the before-and-after measurements of binary size and startup time — you decide whether to go further with data from your own product, not from a benchmark blog post. ### Production Precedent Add-to-app has been a supported Flutter integration model for years, and its best-known user is Google Pay, which adopted Flutter inside an existing native app with hundreds of millions of users before committing further. The pattern is the one this page describes: embed, ship one surface, measure, then decide. Our own delivery record with it comes from [migration work](https://nerdy.pro/services/flutter-app-development/flutter-migration), where the same module-behind-a-router machinery carries whole apps across; adoption uses that machinery with a lighter commitment. ### How We Work **Fixed-scope groundwork and pilot.** We integrate the module into both hosts, port the design tokens, define the channel contracts, and ship the first feature — a bounded engagement with the measurements as part of the deliverable. Our [development process](https://nerdy.pro/blog/flutter-app-development-process) describes how we run scoped work week to week. **Embedded engineers.** Our Flutter engineers join your native team, in your repo and your sprints, and build out the module alongside the people who own the host — see [team augmentation](https://nerdy.pro/services/team-augmentation). This is the natural shape once the pilot has proven out and the module becomes a place where your roadmap regularly lands. Either way, we send a scoped quote within two business days of understanding the requirements. ### What Add-to-App Integration Costs The groundwork-plus-pilot engagement is quoted after we see the app: the integration cost is driven by the host — its navigation architecture, its build system, how its shared services are exposed — far more than by the pilot feature itself, so a number produced before looking at the codebase would be fiction. As an anchor, a bounded pilot lands well below what a standalone app costs — the published tiers on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page start at $15–30K for a full MVP, and a pilot feature inside an existing host is a fraction of that scope. For the embedded model, the published [team augmentation](https://nerdy.pro/services/team-augmentation) rate applies: $10–14K per senior engineer per month. And if the roadmap's honest endgame is an app that is entirely Flutter, price it as what it is — a [migration](https://nerdy.pro/services/flutter-app-development/flutter-migration), which that page anchors against a full rebuild, not as an adoption that outgrew its name. ### Frequently Asked Questions Common questions from teams considering Flutter inside an existing native app. #### Can you add Flutter to an existing iOS or Android app? Yes — that is precisely what Flutter's add-to-app integration model exists for. A Flutter module is embedded into your existing Swift or Kotlin app, wired into its navigation, authentication, and analytics through platform channels, and renders the screens you choose to build in it. Your app keeps shipping unchanged throughout; the module is added alongside the code you have, not instead of it. Each Flutter screen is written once and runs inside both your iOS and Android apps. #### Do we have to migrate the whole app to Flutter? No. Adoption and migration share the same mechanics — a Flutter module behind a shared router — but a migration commits to retiring the native app at the end, and adoption never has to. Every feature boundary is a legitimate stopping point: some products stay hybrid indefinitely with Flutter owning a handful of surfaces, some hand the module to their own team, and some convert the momentum into a full migration once the module has earned it. Nothing built during adoption is thrown away if you later go further. #### How much does embedding Flutter add to app size? The Flutter engine and module add real weight — on the order of several megabytes to an Android release build and somewhat more on iOS. The precise number depends on your app and what the module contains, which is why our pilot engagement includes before-and-after measurements of binary size and startup time on your actual product. You make the adoption decision from your own numbers, not from a generic benchmark. #### Does add-to-app make sense for a React Native app? Mechanically it works — Flutter can be embedded into a React Native host the same way as into a fully native one. Strategically it rarely makes sense: you would be running two cross-platform runtimes, two bridges, and three UI idioms in one binary, which compounds the maintenance problem instead of solving it. For a React Native team unhappy with the status quo, the realistic options are staying put or migrating to Flutter screen by screen — and our migration page opens by telling you who should do neither. #### Is add-to-app production-proven? Yes. Add-to-app has been a supported Flutter integration model for years, and its best-known adopter is Google Pay, which embedded Flutter inside a native app serving hundreds of millions of users before extending its commitment. The pattern of embedding a module, shipping one surface, measuring, and then deciding is well trodden. The engineering risks that remain are the integration details — engine lifecycle, navigation across the boundary, duplicated services — which is exactly the work a structured groundwork engagement front-loads. #### Do our native engineers need to learn Dart? Not for the pilot — we build it, and your team reviews the module boundary rather than the Dart inside it. If Flutter adoption grows, some of your engineers will want to work in the module, and Dart is a small step from Swift or Kotlin: strongly typed, garbage collected, and closer to what they already write than JavaScript would be. The embedded-engineer model exists for exactly this transition — our people build alongside yours until the module is a codebase your team owns rather than one it hosts. Read the [migration page](https://nerdy.pro/services/flutter-app-development/flutter-migration) if the destination is all-Flutter, [team augmentation](https://nerdy.pro/services/team-augmentation) for the embedded model, or start from the [Flutter app development](https://nerdy.pro/services/flutter-app-development) pillar. ## Admin Panels & Dashboards https://nerdy.pro/services/web-development/admin-dashboards Admin panel development is the engineering of the interfaces a business runs on rather than the ones it markets: the dashboard where an operations team watches the numbers, the configuration panel where a manager changes what the product does, the back office where support looks up an account and fixes it. These tools have no landing page and no app store listing. They also decide how many people it takes to run the company. We build them on Vue and Nuxt with TypeScript end to end, the same stack as our public [web development](https://nerdy.pro/services/web-development) work, tuned for a different goal. An admin tool is measured in operator-minutes saved and mistakes prevented, not in bounce rates, and that changes what you engineer for. #### The products we build - **Operations dashboards** — live views over the data a team acts on: orders, sessions, payouts, alerts, with the filters and drill-downs that turn a wall of rows into a decision - **Configuration panels** — interfaces where changing a setting changes the product: pricing rules, feature flags, campaign parameters, evaluated by the backend at runtime - **Back offices for mobile products** — the web-side counterpart of an app, like the admin UI behind the [white-label fleet](https://nerdy.pro/services/whitelabel-app-development) we operate; the customer-facing web surface is its own page, [companion web apps](https://nerdy.pro/services/web-development/companion-web-app) - **Support and account tooling** — look up a user, see what they saw, fix what went wrong, with every action logged - **Merchant and partner portals** — the restricted outside-facing cousin of an admin panel, where a partner manages their own slice of your platform ### Who Builds Your Admin Panel Our clearest proof is a back office that runs a real fleet. #### The admin UI that launches branded apps For our [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform), launching a new white-label brand used to be an engineering errand: configuration, bundle identifiers, signing assets, store metadata, a new CI target. We collapsed all of it into one Nuxt application backed by PHP services and PostgreSQL. A manager fills in a form (brand name, theme, assets, features) and the system generates the configuration bundle, registers the app in the CI/CD pipeline, and produces signed iOS and Android builds. Launching a brand went from a multi-day engineering task to a self-service form operated by people who do not write code. That is the standard we hold admin tooling to: the panel drives the machinery rather than describing it. #### Built by the people who build the backend An admin panel is only as good as its API, and we rarely build one without the other. The same engagement that produced the admin UI above produced the PHP services behind it; [Formtastic's](https://nerdy.pro/portfolio/formtastic) Nuxt frontend runs against a Django and Go backend we evolved alongside it. When one team owns both sides, "the frontend needs an endpoint for this" is a morning's work rather than a cross-team negotiation, and the panel's validation rules match what the backend actually enforces. ### What Admin Tools Actually Demand Five concerns come up in every back-office build. #### Authorization that matches the org chart A login screen is not access control. Real admin tooling needs roles (who can view, who can edit, who can approve) enforced on the server, with the UI merely reflecting what the API already refuses. We build role-based access control into the API layer first, so a crafted request cannot do what a hidden button would not allow. #### Forms that drive real machinery When submitting a form generates configuration, moves money, or triggers a build pipeline, the form inherits the engineering standards of the machinery behind it: validation that matches what the backend enforces, idempotent submission so a double click cannot launch two brands, and clear, specific failure states. The happy path is the easy part. #### Tables that survive real data Every admin panel demos beautifully with twelve rows and dies at fifty thousand. We paginate on the server, push filtering and search into the database where the indexes are, and keep sorting, filtering, and export responsive at the volumes your operation will actually reach. #### An audit trail from day one Who changed the payout rate, when, and from what to what? In an admin tool, that question is a feature rather than forensics. Actions are logged with actor, timestamp, and before-and-after values, because a back office where changes are anonymous becomes a place where nobody is allowed to change anything. #### Boring, deliberately Internal tools do not need SEO, and mostly do not need server-side rendering. A plain Vue single-page app behind a login is often the right call, and we will say so rather than invoice for a framework you do not need. Where Nuxt earns its place, with its server routes standing in for a separate backend or public and internal surfaces sharing one codebase, we use it, as we did for the fleet admin UI. The choice is an engineering decision, made explicit in the proposal. ### How We Work **Fixed-scope build.** We take the tool from workflow mapping to deployment, including the unglamorous parts like data migration from the spreadsheets it replaces. **Staff augmentation.** Our engineers join your team and build the back office alongside your product — see [team augmentation](https://nerdy.pro/services/team-augmentation). Either way, we send a scoped quote within two business days of understanding the requirements. ### What an Admin Panel Costs An admin panel is an authenticated web application, and it is priced from the same published tiers as the [web development](https://nerdy.pro/services/web-development) page: a focused back office with authentication, role-based access, and the core tables and forms sits in the web app tier at $15,000 – $40,000, while a multi-tenant portal or a panel woven into a larger platform prices as SaaS-grade work from $50,000+. These are the same keys that render the pillar's pricing table, so the two cannot drift. What moves the number within the range: how many roles and workflows the tool covers, how much of the backend already exists, and whether the panel merely reads your systems or operates them. ### Frequently Asked Questions Common questions from teams commissioning admin panels and internal dashboards. #### What is the difference between an admin panel and a dashboard? A dashboard shows: live metrics, tables, drill-downs that support a decision. An admin panel acts, because its forms and controls change what the product does, whether that is configuration, content, pricing, or user accounts. Most real back offices are both, and the engineering difference matters: a dashboard's hard problems are queries and data volume, while an admin panel's hard problems are authorization, validation, and making sure a submitted form cannot do damage. We build them as one tool with both disciplines applied. #### How much does an admin panel cost to build? A focused back office with authentication, role-based access control, and the core tables and forms over an existing API typically runs 15,000 to 40,000 US dollars over one to three months, the same published web app tier as on our web development page. A multi-tenant partner portal or a panel that orchestrates real machinery prices as SaaS-grade work upward of 50,000. The main cost drivers are the number of distinct roles and workflows, not the number of screens. #### Do you build the backend too, or just the interface? Both, and usually together. An admin panel is only as good as its API, so the engagement typically covers the endpoints, the authorization layer, and the interface as one piece of work. For the white-label platform we operate, the same team built the Nuxt admin UI and the PHP services behind it, which is why a form submission there can safely generate configuration and trigger signed mobile builds. If you already have an API, we build against it and tell you honestly where it needs to change. #### Vue or Nuxt for an internal tool? For a tool that lives entirely behind a login, a plain Vue single-page app is often the honest answer: nothing needs indexing, so server-side rendering adds moving parts without adding value. Nuxt earns its place when its server routes can stand in for a separate backend on a smaller product, or when internal and public surfaces share one codebase. We work in both and make the choice explicit in the proposal rather than defaulting to the heavier tool. #### Can non-technical staff operate the tools you build? That is the design goal we hold ourselves to. The admin UI we built for a white-label app platform lets managers with no programming skills launch a fully branded, store-ready mobile app by filling in a form: the system generates configuration, registers the CI pipeline target, and produces signed builds. The measure of a back office is whether it removes the engineer from the loop, and we design every workflow against that standard. #### How do you keep an admin panel secure? Authorization enforced on the server, never just hidden in the interface; sessions in httpOnly, secure, SameSite cookies; role-based access control at the API layer; and an audit trail recording who changed what and when. Admin tools concentrate privileges, which makes them a target. If you have an existing panel you are unsure about, our AI code audit reviews authentication, session handling, and data exposure as a dedicated engagement. See [how the fleet's admin UI works](https://nerdy.pro/blog/white-label-app-platform-flutter) in the platform write-up, read about the [white-label service](https://nerdy.pro/services/whitelabel-app-development) it belongs to, or start from the [web development](https://nerdy.pro/services/web-development) pillar. ## AI Code Audit https://nerdy.pro/services/ai-code-audit An AI code audit is an independent review of a codebase — AI-generated or hand-written — that checks it for security vulnerabilities, architectural weaknesses, and production-readiness gaps before real users find them. It ends with a severity-ranked findings report and a clear path to fix what it finds. Most of what we audit today started in Cursor, Bolt, Lovable, Claude, or ChatGPT — fast to prototype, rarely production-ready on day one. But the audit itself is not limited to AI output: the same categories of risk, and the same process, apply to a codebase nobody used AI to write. Whether you're a team shipping fast with AI tools and want a check before the next release, or a founder who inherited an AI-built app and needs to know what's actually in it, you get the same audit. ### What We Audit Seven categories, every time — not just the security scan most "AI code review" tools stop at. #### What Every Audit Covers - Security: OWASP Top 10 vulnerabilities, injection attacks, authentication bypass, exposed secrets, and insecure data storage. - Architecture: Service boundaries, data flow, and coupling — whether the design holds up under real users and real load. - Performance: Database queries, N+1 problems, memory leaks, and capacity under production traffic. - Correctness: Business logic, edge cases, and the bugs that only surface once real data hits the system. - Dependencies: Outdated packages, known CVEs, and licensing risk sitting in the dependency tree. - Test Coverage: What is tested, what is not, and what breaks silently the next time it changes. - AI-Generated-Code Smells: Duplicated logic, hallucinated APIs, and the inconsistent patterns an unsupervised agent leaves behind. ### What You Get An audit is not a PDF that sits in a drawer. Three things come out of it. #### The Audit Deliverable - Findings Report: Every issue ranked by severity — critical, high, medium, low — with a plain-language risk explanation and a specific fix. - Sample Audit Report: A redacted example of a real audit, so you see exactly what you are paying for before you send us any code. - Remediation Call: A walkthrough of every finding with our engineers, and a straight answer on what to fix first. > **See a Sample Before You Commit** > > Want to see what a real finding looks like — severity, risk explanation, fix — before you send us your code? [Request a redacted sample audit report](https://nerdy.pro/contact?intent=ai-code-audit) and we'll send one over. ### Two Ways to Start People come to us for an audit for one of two reasons. The findings report looks the same either way — what differs is where we start looking. #### Auditing an AI-Generated or Vibe-Coded App You (or a non-technical founder, or a contractor) used Cursor, Bolt, Lovable, Claude, or ChatGPT to build something that works — it demos well, early users like it, and now it needs to survive contact with the real world. We start where these tools consistently fail: hardcoded secrets, authentication that checks the box but not the request, and API usage that was never built to a budget. Our playbook for this exact handoff is in [Shipping an AI-Built Prototype to Production](https://nerdy.pro/blog/ai-prototype-to-production). #### AI-Accelerated Audit of Any Codebase Your codebase does not have to be AI-generated for this to apply. We run the same automated analysis — static analysis, dependency scanning, secrets detection — against any stack, AI-assisted or not, which is what lets us turn an audit around in weeks instead of months. The manual review on top is where our engineers actually read your architecture and business logic; the automation just means less of the budget goes to finding the obvious stuff by hand. ### What Goes Wrong in AI-Generated Code We've audited dozens of AI-generated codebases. The same categories of problems appear in nearly every one — systematic blind spots, not edge cases. - **45%** Fail Security Tests: AI-generated code samples fail OWASP security checks (Veracode, 2025) - **5-20x** API Overspend: Typical cost overrun from unoptimized paid API usage - **2x** More Secret Leaks: AI-assisted developers expose credentials nearly twice as often (Apiiro, 2025) - **2-4 weeks** Audit Timeline: Time to identify and fix critical issues These numbers come from independent research: [Veracode tested 100+ LLMs](https://www.veracode.com/blog/genai-code-security-report/) and found 45% of generated code fails security tests. [Apiiro's 2025 analysis](https://apiiro.com/blog/4x-velocity-10x-vulnerabilities-ai-coding-assistants-are-shipping-more-risks/) showed AI-assisted developers expose credentials nearly twice as often. [NYU researchers](https://arxiv.org/pdf/2108.09293) found ~40% of Copilot-generated programs contained vulnerabilities, while [Stanford's study](https://ee.stanford.edu/dan-boneh-and-team-find-relying-ai-more-likely-make-your-code-buggier) confirmed developers using AI assistants produce less secure code — and are more confident it's safe. Our own review of agent-written Flutter is in [How AI Agents Struggle with Flutter](https://nerdy.pro/blog/ai-agents-struggle-with-flutter): duplicated state, no tests, and UI that never got a second look from a human. #### Security Vulnerabilities 1. Exposed API Keys and Secrets AI tools frequently hardcode API keys, database credentials, and third-party service tokens directly in source code. These end up in public repositories, client-side bundles, or environment files that ship to production. One leaked OpenAI key can generate thousands of dollars in unauthorized usage within hours. 2. Missing Authentication and Authorization AI-generated apps often implement authentication at the surface level — a login screen exists, but the backend doesn't actually verify permissions. API endpoints accept any request. Admin routes are accessible without role checks. User A can see User B's data by changing an ID in the URL. 3. Injection Attacks SQL injection, XSS, and prompt injection are pervasive in AI-built code. AI models generate code that concatenates user input directly into queries, HTML, or LLM prompts without sanitization. A single vulnerable endpoint can compromise your entire database or allow attackers to manipulate your AI's behavior. 4. Insecure Data Storage Personal data stored in plain text, session tokens in localStorage, passwords hashed with MD5 or not hashed at all. AI tools default to the simplest implementation, which is rarely the secure one. GDPR and privacy compliance is typically absent. Our guide to [encryption in production applications](https://nerdy.pro/blog/encryption-explained) covers how data should actually be protected at rest and in transit. #### API Cost Optimization 1. Redundant API Calls The most expensive problem we find. AI-generated apps make unnecessary calls to paid third-party APIs — geocoding services, financial data providers, AI models, verification APIs. They call the same endpoint multiple times for the same data, fail to cache responses, and make requests that could be avoided entirely with simple local logic. We routinely find apps where 60-80% of API spend is wasted. 2. Missing Rate Limiting and Budgets No per-user rate limits. No daily spend caps. No circuit breakers when API costs spike. A single user — or a bot — can burn through your entire monthly budget in a day. AI tools never implement cost controls because they have no concept of your business model or the pricing tiers of the services you integrate. 3. No Caching or Batching Fetching the same exchange rate, IP geolocation, or user profile from a paid API on every single request instead of caching it. Making individual API calls in a loop instead of using batch endpoints. Each unnecessary request costs money, and at scale these add up to thousands of dollars per month. #### Data Leaks and Privacy 1. Logging Sensitive Information AI-generated code loves verbose logging. User emails, passwords, payment details, and personal data end up in application logs, error tracking services, and third-party analytics. These logs are often accessible without authentication and retained indefinitely. 2. Oversharing Through APIs API responses that return entire user objects — including hashed passwords, internal IDs, email addresses, and metadata — when the frontend only needs a display name. GraphQL endpoints without depth limits that allow attackers to extract your entire data model. 3. Third-Party Data Exposure AI tools integrate analytics, error tracking, and monitoring services without considering what data flows to them. User behavior, personal information, and business data end up in third-party systems without consent, violating GDPR and other privacy regulations. ![Deploying unaudited AI-generated code to production](https://nerdy.pro/services/ai-transition-example.webp) *What happens when AI-generated code goes to production without an audit* ### Who's Behind the Audit Nerdy Production is led by [Ilya Nixan](https://nerdy.pro/team/nixan), previously CTO of QIWI, one of the largest payment platforms in its market, where he ran roughly 12 engineering teams covering everything from web products down to card processing and PCI-DSS scope. That is the standard our audits are held to: not "does the code run", but "would this survive a review by someone who has carried compliance scope and had to answer for a production incident." We do not hold PCI-DSS or SOC 2 certification ourselves, and we will tell you plainly when a finding is outside what an audit can certify — but the person setting the bar for what counts as a critical finding has operated regulated infrastructure, not just read about it. Our engineers work with AI coding tools daily, which is exactly why we know where they fail. [How AI Agents Struggle with Flutter](https://nerdy.pro/blog/ai-agents-struggle-with-flutter) walks through the specific gaps an unsupervised agent leaves behind — duplicated state, missing tests, UI that never got a second look — the same patterns we look for in every audit, regardless of language or framework. ### How an Audit Works We deliver a complete audit with fixes you can act on, not just a list of problems. 1. Codebase Access and Scope You grant us read access to your repository and deployed environment. We define the audit scope based on your priorities: full audit or focused on specific areas (security, costs, or data privacy). No changes are made to your code during the audit phase. 2. Automated Analysis We run static analysis, dependency vulnerability scans, secrets detection, and API cost profiling against your codebase. This catches the low-hanging fruit — known vulnerabilities, exposed credentials, outdated packages with security patches, and obvious performance issues. 3. Manual Expert Review Our engineers manually review the architecture, business logic, authentication flows, API integrations, and data handling. This is where we find the problems that automated tools miss: logic flaws, authorization gaps, cost optimization opportunities, and design issues that will cause problems at scale. 4. Severity-Ranked Report We deliver a detailed report with every finding categorized by severity (critical, high, medium, low) and type (security, cost, privacy, performance). Each finding includes a clear explanation of the risk, proof of concept where applicable, and a specific fix recommendation with code examples. 5. Remediation — Your Choice You pick who fixes it. We can implement every fix ourselves as a fixed-scope engagement, with critical security patches first and you reviewing every change before it's merged. Or, if you'd rather your own team own the fixes, we can embed an engineer who already knows your findings report through [team augmentation](https://nerdy.pro/services/team-augmentation) instead of handing you a document and disappearing. Either way, the result is a codebase you can deploy with confidence. ### AI-Generated Code vs Production-Ready Code What changes when your codebase goes through a professional audit | Aspect | After Audit (Production-Ready) | AI-Generated Code | Built from Scratch | | --- | --- | --- | --- | | Security | Hardened | Surface-Level | Varies by Team | | API Costs | Optimized | 5-20x Overspend | Usually Optimized | | Data Privacy | GDPR-Compliant | Data Leaks Common | Depends on Process | | Time to Production | 2-4 Weeks | Already Running | 3-6 Months | | Cost to Launch | Low (Audit Fee) | Free (Risky) | High (Full Dev) | | Error Handling | Graceful Recovery | Crashes in Edge Cases | Usually Handled | | Scalability | Load-Tested | Untested | Architected for Scale | | Monitoring | Full Observability | None or Excessive | Standard Setup | > **The bottom line:** AI-generated code gets you 80% of the way there in 5% of the time. But that last 20% — security, cost optimization, data privacy, error handling — is what separates a demo from a production application. An audit bridges the gap without throwing away your AI-built foundation. ### AI Code Audit Pricing Transparent pricing based on your codebase size and audit scope #### Security Focused Critical vulnerabilities only **$2–5K** 1-2 weeks - OWASP Top 10 vulnerability scan - Secrets and credentials detection - Authentication & authorization review - Dependency vulnerability check - Prioritized findings report - Fix recommendations with code examples [Get Started](https://nerdy.pro/contact?intent=ai-code-audit) #### Cost Optimization Reduce your API spend **$3–7K** 1-2 weeks - Paid API usage profiling - Redundant call identification - Caching & batching strategy design - Rate limiting implementation plan - Per-request cost optimization - Projected monthly savings report [Get Started](https://nerdy.pro/contact?intent=ai-code-audit) #### Full Audit (Popular) Complete production readiness **$5–15K** 2-4 weeks - Everything in Security Focused - Everything in Cost Optimization - Data privacy & GDPR review - Performance & scalability analysis - Infrastructure audit - Detailed report + fix implementation [Get Started](https://nerdy.pro/contact?intent=ai-code-audit) #### Ongoing Support Continuous audit & monitoring **$2K+** per month - Monthly security scans - API cost monitoring & alerts - Dependency update reviews - New feature security review - Priority incident response - Direct Slack/Telegram access [Get Started](https://nerdy.pro/contact?intent=ai-code-audit) > **Custom Pricing Available** > > Pricing depends on codebase size, number of services and integrations, and audit scope. A small single-service app with one AI integration will be at the lower end. A multi-service platform with several AI providers, payment processing, and user data will require a more thorough review. Contact us for a free initial assessment. ### Frequently Asked Questions Common questions about auditing AI-generated and production codebases #### My app was built with Cursor / Bolt / Lovable — do you work with these? Yes. We audit code regardless of how it was generated. Whether you used Cursor, Bolt, Lovable, Claude, ChatGPT, GitHub Copilot, or any combination of AI tools, the audit process is the same. The output is code, and that is what we review. We have experience with all major AI coding tools and know their common failure patterns. #### Do you audit codebases that were not built with AI? Yes. The audit finds the same categories of risk — security, architecture, performance, correctness, dependencies, test coverage — whether or not AI wrote any of the code. What we call an AI-accelerated audit uses automated analysis to move through any codebase faster, so a hand-written app gets the same thorough review on the same timeline. #### Will you need to take the app down during the audit? No. The audit is entirely non-disruptive. We review your source code and may run read-only tests against a staging environment. Your production application continues running normally throughout the process. If we find a critical vulnerability that poses immediate risk, we will notify you immediately so you can decide how to proceed. #### How much can I actually save on API costs? In our experience, AI-generated applications overspend on paid API calls by 5-20x. The most common savings come from eliminating redundant calls to services like geocoding, financial data, or AI providers, implementing response caching, batching requests, and adding rate limits to prevent abuse. We have seen monthly API bills drop from $5,000 to under $500 after optimization, though results depend on your specific usage patterns. #### Can you also fix the issues you find, or just report them? Both. The audit report includes specific fix recommendations with code examples for every finding. If you prefer, we can implement all fixes ourselves — this is included in the Full Audit tier and available as an add-on for other tiers. If you would rather your own team do the fixing, we can embed an engineer through staff augmentation instead. You review and approve every change before it is merged. #### Can I see a sample audit report before I commit? Yes. Request a redacted sample audit report through the contact form and we will send one over — the same severity-ranked format, risk explanations, and fix recommendations a real engagement produces, with client-identifying details removed. #### I am not technical — will I understand the audit report? Yes. Every finding includes a plain-language explanation of the risk and its business impact, not just technical jargon. We categorize issues by severity so you know what needs immediate attention versus what can wait. We also include an executive summary at the top of the report with the key takeaways and recommended action plan. ## Chatbot to Standalone App https://nerdy.pro/services/chatbot-to-app ### Why Transition from a Chatbot to a Standalone App Chatbots are a great way to validate an idea quickly. Telegram bots, WhatsApp integrations, and web chat widgets let you test demand with minimal investment. But once your user base grows, the limitations become clear: restricted UI, platform dependency, no push notifications on your terms, and zero brand presence on the home screen. A standalone app removes every one of these constraints while preserving the conversational experience your users already know. - **3-5x** User Engagement: Standalone apps drive higher retention than bots - **24/7** Home Screen Presence: Your brand is always one tap away - **100%** Notification Control: Full push notification capabilities - **Full** Data Ownership: Your data, your infrastructure, your rules ### When It's Time to Move Beyond a Bot Not every chatbot needs to become an app. But there are clear signals that your product has outgrown its messaging platform: #### Signs You've Outgrown Your Bot 1. User Interface Limitations Your bot does more than simple text exchanges. Users need rich media, interactive elements, charts, or complex multi-step workflows — things that messaging platforms handle poorly. Every workaround you build inside the bot's constraints is time spent fighting the platform instead of building your product. 2. Platform Risk Your entire business runs on someone else's infrastructure. A policy change, an API deprecation, or a ban can shut you down overnight. Telegram bot API rate limits, WhatsApp Business restrictions, and platform review processes add unpredictable risk to your growth. 3. Branding and User Experience Bots live inside another app's interface. You can't control the fonts, colors, animations, or layout. There's no splash screen, no onboarding flow, no way to create a cohesive brand experience. Your product looks like every other bot — indistinguishable in a crowded channel. 4. Monetization Constraints In-app purchases, subscriptions, and payment integrations are either unavailable or severely limited in most messaging platforms. A standalone app unlocks the full monetization toolkit: App Store and Google Play subscriptions, native payment flows, and direct billing. #### What You Gain with a Standalone App - Custom UI/UX: Complete control over every pixel — animations, layouts, and interactions tailored to your product - Push Notifications: Targeted, scheduled notifications without platform restrictions or rate limits - Offline Access: Users can access content and features without an internet connection - App Store Presence: Discoverability through App Store and Google Play search - Native Device Features: Camera, biometrics, GPS, sensors, and platform-specific integrations - Analytics & Attribution: Full user behavior tracking, funnel analysis, and marketing attribution ### How We Handle the Transition We've built this exact transition for real products — see [how we ship a Telegram bot as an app in six weeks](https://nerdy.pro/blog/telegram-bot-to-app-6-weeks) for a week-by-week walkthrough. The same chat-heavy Flutter foundation powers apps like [Arcana](https://nerdy.pro/portfolio/arcana) and [YouMi](https://nerdy.pro/portfolio/youmi). Our approach preserves what works in your bot while removing its limitations. #### Our Process 1. Bot Audit & Feature Mapping We analyze your existing bot: every command, every flow, every integration. We map each feature to its native app equivalent and identify opportunities that only a standalone app can unlock. The result is a detailed specification that becomes the foundation for development. 2. Architecture Design We design the app architecture to support your existing backend. If your bot talks to a REST API, the app talks to the same API. If it uses WebSocket connections, we implement proper connection lifecycle management with reconnection logic, message queuing, and state synchronization. No backend rewrite required unless you want one. 3. UI/UX Design We translate your conversational flows into native app screens. Chat-based interactions stay as chat where it makes sense. But now you can also add dashboards, settings screens, media galleries, and any other UI pattern that was impossible inside a bot. The design stays familiar to existing users while giving the experience room it never had inside a bot. 4. Development & Testing We build with Flutter for cross-platform delivery. One codebase, two platforms, native performance. Our development process includes continuous integration, automated testing, and regular demo builds so you can see progress every week. 5. Migration & Launch We handle App Store and Google Play submissions, set up analytics, configure push notifications, and plan the migration path for your existing users. Your bot can continue running during the transition — we help you move users gradually with deep links and in-bot announcements. ### Chatbot vs Standalone App See what changes when you move from a messaging platform to your own application | Feature | Standalone App (Recommended) | Telegram Bot | WhatsApp Bot | | --- | --- | --- | --- | | User Interface | Fully Custom | Text + Buttons Only | Text + Lists Only | | Push Notifications | Full Control | Limited | Template-Based | | Offline Access | Yes | No | No | | Branding | Complete Control | Bot Avatar Only | Business Profile | | Monetization | Full (IAP, Subs) | Telegram Stars | None Built-in | | Device Features | All Native APIs | Camera, Location | Very Limited | | Analytics | Full Tracking | Basic Stats | Basic Stats | | Platform Risk | Zero | High (API changes) | High (Policy changes) | | App Store Presence | Full SEO & ASO | None | None | | Data Ownership | Complete | Shared with Telegram | Shared with Meta | > **Bottom line:** A chatbot is an excellent starting point and validation tool. But when your product needs custom UI, reliable notifications, monetization, and independence from third-party platforms, a standalone app is the natural next step. You don't have to abandon the bot: it can keep running as an entry point while the app gives your product room to grow. ### Bot-to-App Migration Readiness Checklist Before you commit to a single line of code, run your bot through this checklist. The more boxes you can tick, the faster and cheaper your migration will be. If you're stuck on any of them, that's exactly what a free migration audit is for. ##### Product signals — is it time to move? - You have an active, growing user base (a few thousand monthly users or more) - Users regularly ask for features your bot cannot deliver - You are hitting platform rate limits, policy reviews, or API restrictions - You want push notifications you fully control — timing, targeting, and content - You need to monetize with subscriptions or in-app purchases - Your brand deserves a presence on the home screen, not inside a third-party app ##### Technical readiness — how hard is the lift? - Your bot already talks to a backend API (REST, GraphQL, or WebSocket) - Business logic lives on your server, not hardcoded inside bot handlers - You can export or sync existing user accounts and conversation history - You have a clear list of every command, flow, and integration the bot supports - Authentication can be reused or mapped to phone, email, or social login ##### Launch readiness — can you ship and migrate? - You have (or can create) Apple Developer and Google Play accounts - You can announce the app to existing users through the bot itself - You have a plan for running the bot and app in parallel during migration - You know which metrics define success (retention, conversion, revenue) [Get a Free Migration Audit](https://nerdy.pro/contact?intent=chatbot-to-app) > **Not sure how many boxes you ticked?** > > You don't need every item checked to start. Most teams come to us with gaps — no separate backend, no migration plan, no idea which features matter most. The audit turns this checklist into a concrete, prioritized roadmap with a timeline and a fixed-scope quote. ### Chatbot to Standalone App Pricing Transparent pricing for your bot-to-app transition #### Simple Bot Straightforward transition **$5–10K** 4-6 weeks - iOS & Android apps - Core bot features preserved - Basic chat interface - Push notifications - User authentication - App store deployment - Bot-to-app user migration [Get Started](https://nerdy.pro/contact?intent=chatbot-to-app) #### Feature-Rich Bot Complex flows & integrations **$10–22.5K** 2-3 months - iOS & Android apps - All bot features + new screens - Custom UI/UX design - Real-time messaging - Payment integration - Offline functionality - Analytics dashboard - Admin panel [Get Started](https://nerdy.pro/contact?intent=chatbot-to-app) #### AI-Powered Bot (Popular) Bots with AI/ML backends **$17.5–35K** 3-4 months - iOS & Android apps - AI chat with streaming responses - Real-time markdown rendering - Media-rich content display - Subscription management - Performance optimization - Offline caching - Usage analytics & insights [Get Started](https://nerdy.pro/contact?intent=chatbot-to-app) #### Enterprise Bot Multi-platform ecosystem **$35K+** 4+ months - All platforms + Web - Complex workflow migration - Multi-user/role support - Enterprise integrations - Custom backend development - Advanced security - White-label options - Dedicated support [Get Started](https://nerdy.pro/contact?intent=chatbot-to-app) > **Custom Pricing Available** > > Every bot-to-app transition is different. Pricing depends on your bot's complexity, the number of features, backend requirements, and desired timeline. We'll provide a detailed quote after reviewing your existing bot. Contact us for a free consultation and accurate estimate. ### Frequently Asked Questions Common questions about transitioning from a chatbot to a standalone app #### Can I keep my bot running alongside the app? Yes. We recommend a gradual migration strategy. Your bot continues to operate while the app launches, and we help you move users over time using in-bot announcements and deep links. Many businesses keep a simplified version of the bot running permanently as an additional entry point. #### Will I need to rebuild my backend? Usually not. If your bot already has a backend API, the app connects to the same endpoints. We adapt the client-side architecture to work with your existing infrastructure. If your bot runs entirely within a platform (like Telegram Bot API with no separate backend), we help you extract the business logic into a proper API layer. #### How long does the transition take? A simple bot with basic features can be transitioned in 4-6 weeks. Feature-rich bots with complex flows, AI integrations, and real-time features typically take 2-4 months. We provide a detailed timeline after auditing your existing bot. #### Why Flutter for the app? Flutter lets us build for iOS and Android from a single codebase, which cuts development time and cost by 30-40% compared to building two native apps. Flutter compiles to native ARM code, so there is no performance compromise. It is ideal for chat-heavy apps that need smooth scrolling, real-time updates, and rich media rendering. #### What happens to my existing users? We design a migration path that minimizes friction. In-bot messages announce the app, deep links take users directly to the App Store or Google Play, and we can sync user accounts between the bot and app. Existing data and conversation history can be preserved and imported into the new app. ## Companion Web Apps https://nerdy.pro/services/web-development/companion-web-app A companion web app is the browser-side surface of a product that lives primarily in a mobile app: the place where a customer signs up before installing, where a shared link opens for someone without the app, where work started on a phone continues on a desktop. It is the same product, on the same API, shaped for what a browser is actually good at, rather than a port of the app or a brochure about it. Most agencies build one side or the other. We ship Flutter apps, web frontends, and the backend services both consume. That changes the engineering, because the question "which surface should this feature live on" gets answered by one team looking at one system instead of negotiated across two contractors. #### The products we build - **Customer web apps alongside mobile apps** — the full product in the browser where that makes sense, or the deliberate subset where it does not - **Public, shareable surfaces** — the pages a shared link resolves to, rendered and indexable, that hand off into the app when it is installed - **Real-time web clients** — live data in the browser from the same feed the apps consume - **Back offices for the same product** — the operator-facing side is its own discipline, covered on [admin panels & dashboards](https://nerdy.pro/services/web-development/admin-dashboards) - **The web side built on Nuxt** — when the companion surface must rank, the machinery is our [Nuxt development](https://nerdy.pro/services/web-development/nuxt-development) practice ### Who Builds Your Companion Web App Two products in production show the pattern, each with an app and a web surface sharing one backend. #### Formtastic — one platform, three clients [Formtastic](https://nerdy.pro/portfolio/formtastic) runs as a Nuxt web application plus Flutter apps for iOS, iPad, and Android, all against the same Django and Go backend. Features land differently per surface. Voice-to-text on the web records audio in the browser and sends it to a transcription endpoint, because a backend was already there to extend; the mobile apps bundle Whisper on-device, because job sites have no signal. Same feature, two implementations, each honest about its surface. That judgment, more than framework skill, is what a two-surface product actually requires. #### ExtraETF — one real-time feed, every client For [ExtraETF](https://nerdy.pro/portfolio/extraetf), we built the Go WebSocket service that broadcasts live market data to both web and mobile clients — thousands of simultaneous connections, updates conflated to the cadence a human watching a price actually needs. We built the mobile apps and the real-time layer; the web client consuming it belongs to the client's own team. A well-designed service feeds surfaces you will never build yourself. ### What a Two-Surface Product Actually Demands Four disciplines keep an app and a web surface from drifting into two products. #### One API that serves both honestly The moment the web needs "just one special endpoint," you have two backends wearing one name. We design the API as the product's single contract, with the same authentication, the same models, and the same versioning for every client. Where the surfaces genuinely need different shapes of data, that difference is written into the contract rather than patched into a client. #### Deciding what the web version is The expensive mistake is "the app, but in a browser." A companion surface earns its budget by doing what the app cannot: being reachable from a search result, opening instantly from a shared link, offering a keyboard and a big screen for heavy editing. We scope the web surface feature by feature against that test, and cutting a feature from the web version is a decision we argue for out loud rather than a gap you discover later. #### The handoff between web and app A shared link should open the app when it is installed, the web page when it is not, and never a broken interstitial. Universal links on iOS and app links on Android, the fallback pages behind them, and the sign-up flow that starts in a browser and continues in the app. This seam is where two-surface products most visibly leak users, so we engineer it deliberately instead of assuming it works. #### Rendering matched to the page's job The public, shareable pages render on the server so crawlers and link previews see real content; the logged-in workspace behind them renders like the application it is. The trade-offs are laid out on the [web development](https://nerdy.pro/services/web-development) pillar, and the choice is per route, not per project. ### How We Work **Fixed-scope build.** We take the web surface from feature scoping through deployment, against your existing API or one we build alongside it. **Staff augmentation.** Our engineers join the team that owns the product — see [team augmentation](https://nerdy.pro/services/team-augmentation). If the mobile side needs building too, that is our [Flutter practice](https://nerdy.pro/services/flutter-app-development). Either way, we send a scoped quote within two business days of understanding the requirements. ### What a Companion Web App Costs A companion surface is priced from the same published tiers as the [web development](https://nerdy.pro/services/web-development) page: an authenticated web app on your existing API sits in the web app tier at $15,000 – $40,000, while a full second surface with real-time features, payments, or multi-tenant structure prices as SaaS-grade work from $50,000+. These are the same keys that render the pillar's pricing table, so the two cannot drift. What moves the number: how much of the API already exists and how cleanly it was designed for more than one client, how much of the product the web version covers, and whether real-time features ride an existing feed or need one built. ### Frequently Asked Questions Common questions from teams whose product lives in an app and keeps being asked for in a browser. #### Our product is a mobile app. Do we need a web version at all? Not always. The honest answer depends on where your growth leaks. A web surface earns its budget when sign-ups start from search or shared links, when users ask to work on a big screen, or when every shared piece of content currently dead-ends for people without the app. If none of those apply, we will say so; a well-made store listing and deep links may be all the web presence you need. #### Should the web version have every feature the app has? Usually not, and deciding what to leave out is most of the scoping work. The browser wins at reachability (search, shared links, instant access with no install) and at keyboard-and-big-screen work; the app wins at camera, offline, notifications, and being one tap away. Formtastic's voice-to-text is the pattern: the web records in the browser and transcribes on the server, the apps transcribe on-device because job sites have no signal. Same feature, different honest implementations, and some features rightly stay app-only. #### Can you build on our existing backend, or do you need to own it? We build against existing APIs routinely. Formtastic's web frontend runs on the Django backend the product already had, extended where the web needed it. What we will do first is assess whether the API is genuinely multi-client: authentication a browser can use safely, models that do not assume app-only flows, and versioning that lets two clients evolve. Where it falls short we tell you precisely what to change, and can do both sides ourselves when that is faster. #### Can the web app show the same live data as the mobile app? Yes, and it is a feed design question rather than a framework question. For ExtraETF we built the Go WebSocket service that broadcasts live market data to web and mobile clients alike, holding thousands of simultaneous connections and conflating updates to roughly one per instrument per second, the rate a human actually needs. Design the real-time layer once, correctly, and every current and future client consumes the same stream. #### How do shared links decide between the app and the web page? Through universal links on iOS and app links on Android: the same URL opens the app when installed and the web page when not, with the web page carrying the full content rather than an install nag. Done right, the link also survives installation — someone who lands on the web, installs the app, and opens it arrives at the content they were promised. This handoff is where two-surface products lose the most users, so we treat it as a feature with its own engineering budget. #### How much does a companion web app cost? An authenticated web app on an existing API typically runs 15,000 to 40,000 US dollars over one to three months, the published web app tier on our web development page. A full second surface with real-time data, payments, or multi-tenant structure prices as SaaS-grade work upward of 50,000. The biggest lever is your API: one designed for multiple clients keeps the web build lean, while one assuming app-only flows adds backend work we will scope explicitly. See both surfaces in the [Formtastic](https://nerdy.pro/portfolio/formtastic) case study, read how the [ExtraETF real-time feed](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go) fans out to every client, or start from the [web development](https://nerdy.pro/services/web-development) pillar. ## E-commerce App Development https://nerdy.pro/services/flutter-app-development/e-commerce E-commerce app development is the engineering of mobile storefronts — product catalogs, carts, checkouts, order tracking, and the loyalty mechanics that bring a customer back. It differs from ordinary app development in three specific ways: the catalog is bigger than the device (so search, caching, and image delivery decide how the app feels), the checkout is where every defect turns into abandoned revenue, and the same storefront often has to exist several times over — per brand, per market, per currency. Flutter fits retail unusually well because it renders its own UI: a product grid, a story-style promotion, or a custom checkout flow behaves identically on iOS and Android from one codebase, at roughly 30–40% less than building the same storefront natively twice. The parts that touch the platform directly — Apple Pay and Google Pay sheets, some payment-provider SDKs — run through Platform channels, which is routine but real engineering work. #### The products we build - **Storefront apps** — catalog, search, cart, checkout, order tracking, and reviews for a single brand - **Loyalty and cashback apps** — points, rewards, and offers, as in the [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform) we built and run as a fleet - **White-label retail fleets** — one codebase producing a branded app per merchant or market; the platform work is its own [service page](https://nerdy.pro/services/whitelabel-app-development) - **Merchant-side apps** — the counterpart app a seller or store manager uses, which we built for the same platform - **Marketplace storefronts** — where the catalog belongs to many sellers; the two-sided dynamics are covered on our [marketplace page](https://nerdy.pro/services/flutter-app-development/marketplace) ### Who Builds Your E-commerce App We will be straight about what our portfolio contains: no single-brand storefront we can name. What it contains instead is the hard parts of commerce, shipped repeatedly and still in production. #### A fleet of fifteen branded consumer apps For a cashback and loyalty platform under NDA, one Flutter codebase produces [more than fifteen fully branded apps](https://nerdy.pro/portfolio/cashback-loyalty-platform) — each with its own name, theme, and store listing — plus the merchant app on the other side of every transaction and an admin panel that launches a new brand without engineering work. Eight months of build, and the architecture is [written up](https://nerdy.pro/blog/white-label-app-platform-flutter). If your retail strategy involves more than one storefront, this is the exact shape of the problem. #### Payments, operated from the inside Nerdy Production is led by [Ilya Nixan](https://nerdy.pro/team/nixan), previously CTO of QIWI, one of the largest payment platforms in its market, where his teams ran card processing and carried PCI-DSS scope. The relevance to a shop is direct: checkout is a payments integration wearing a storefront, and the person leading your build has operated the other side of that integration. The disciplines that come with it — money as integers in minor units, entitlement state owned by the server, card data kept out of your app's scope — are the same ones our [fintech page](https://nerdy.pro/services/flutter-app-development/fintech) describes in full. #### Catalog mechanics, at marketplace scale [OneTwoDo](https://nerdy.pro/portfolio/onetwodo) is not a shop, but it is a browsable catalog of listings with photos, search, multi-currency pricing, and a feed filtered by language and location — designed and shipped in seven weeks. The mechanics of "show a large collection of things beautifully on a mid-range phone" transfer whole. ### What E-commerce Apps Actually Demand Five concerns come up in every retail build. Here is how we handle each. #### A catalog bigger than the device The catalog lives on the server; the app holds a moving window over it. That sentence hides most of the engineering: paginated queries that keep scroll position stable, search that tolerates typos and returns in milliseconds, images sized and cached so a product grid does not consume a data plan, and a product page that renders instantly from cached list data while the details load behind it. Get this wrong and the app feels slow everywhere while every individual request looks fine. #### A checkout that treats failure as the main path Payments fail — cards decline, 3-D Secure challenges time out, the app is backgrounded mid-payment, the network drops between the provider confirming and your backend hearing about it. A checkout is engineered around those cases, not around the happy path: idempotent order creation so a retry cannot charge twice, payment state owned by the server and merely displayed by the client, and a definite answer on screen for every outcome. "Payments that succeed at the provider but fail to confirm to your backend" is the bug class that costs real money, and it is designed out at the architecture stage. #### Native payment sheets, done natively Apple Pay and Google Pay convert dramatically better than typed card forms on mobile, and both are reached from Flutter through platform channels backed by each platform's own SDK. We wire them alongside the provider's card flow, not instead of it, and we keep raw card data inside the provider's SDK so your PCI scope stays as small as your product allows. #### Prices are money, money is not a float Every price, discount, tax line, and total is integer minor units or a decimal type — never floating point, where `0.1 + 0.2` does not equal `0.3` and a cart total can disagree with the sum of its lines. Rounding rules are explicit and tested at every boundary where money is displayed, transmitted, or stored. The long version is in [Why 0.1 + 0.2 ≠ 0.3](https://nerdy.pro/blog/floating-point-and-money). #### The store rules retail teams discover late Both stores take a cut of digital goods but not of physical ones — selling physical products through your own payment provider is fine, and mixing digital perks into a retail app is where review problems start. Apple's guideline 4.2.6 is the other trap: a fleet of thin, near-identical branded apps is exactly what it exists to reject, and designing a white-label platform to pass it is a specialty of ours rather than a surprise in week twenty. Account deletion, data-safety declarations, and review timelines are budgeted as a phase, the same as on every page of this site. ### Why Flutter for E-commerce | Requirement | How Flutter handles it | | --- | --- | | Both stores, one budget | One codebase, roughly 30–40% less than two native storefronts | | A brand-perfect UI | Flutter draws every pixel, so the design system is yours, not the platform's | | Fast product grids on cheap phones | Native-compiled rendering holds 60 fps where shoppers actually are — mid-range Android | | Seasonal iteration speed | Sub-second hot reload; a promotion screen ships in days, not release cycles | | More than one storefront | The same codebase scales from one brand to a fleet — see the [white-label platform](https://nerdy.pro/services/whitelabel-app-development) | Where native still wins: if your storefront is essentially a wrapper around one platform's capability — an App Clip-led experience, a deep Wallet integration as the product itself — the cross-platform saving is not the point, and we will say so. The general service page is [Flutter app development](https://nerdy.pro/services/flutter-app-development). ### How We Work **Fixed-scope build.** We own delivery end to end — discovery, architecture, build, store submission. Our [development process](https://nerdy.pro/blog/flutter-app-development-process) describes what that looks like week to week. **Staff augmentation.** Our engineers join your existing team, in your repo and your sprints — see [team augmentation](https://nerdy.pro/services/team-augmentation). Either way, we send a scoped quote within two business days of understanding the requirements. ### What an E-commerce App Costs A storefront — catalog and search, cart and checkout, payment gateway integration, order tracking, reviews — is its own tier on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page: $50–90K, over four to six months. A multi-vendor platform, a branded fleet, or deep ERP and inventory integration is enterprise work, running from $90K+. These are the same published tiers as the pillar's pricing table — the same keys render both, so the two cannot drift. What moves a retail number within them: the count of payment methods and markets, how much of the catalog machinery your backend already provides, and whether "one app" is actually a fleet. ### Frequently Asked Questions Common questions from teams building retail and e-commerce products on Flutter. #### Is Flutter good for e-commerce apps? Yes. Flutter renders its own UI, so a product grid, a promotion screen, or a custom checkout looks and behaves identically on iOS and Android from one codebase, typically at 30 to 40 percent less than building two native storefronts. It compiles to native code, which keeps scrolling smooth on the mid-range Android devices where much of retail traffic actually happens. The platform-specific parts — Apple Pay and Google Pay sheets, some payment provider SDKs — are reached through platform channels, which is routine but real engineering work. #### How much does it cost to build an e-commerce app? Our published e-commerce tier covers catalog and search, cart and checkout, payment gateway integration, order tracking, reviews, and inventory management over four to six months — the exact range is on this page and on the Flutter app development page rather than behind a sales call. Multi-vendor platforms and branded app fleets price as enterprise work. What moves the number is the count of payment methods and markets first, and how much catalog machinery your backend already provides second. #### Can a Flutter app use Apple Pay and Google Pay? Yes, through platform channels backed by each platform's own payment SDK, presented as the native payment sheet users recognize. Native sheets convert measurably better than typed card forms on mobile, so we wire them alongside the provider's card flow rather than instead of it. Raw card entry stays inside the payment provider's SDK, which keeps your app out of PCI scope wherever the provider allows it. #### Can Flutter handle a large product catalog? Yes, with the right architecture: the catalog lives on the server and the app holds a paginated window over it, with search on the backend, images sized and cached per screen, and product pages that render instantly from cached list data while details load behind them. The failure mode is not Flutter — it is treating a hundred-thousand-item catalog as a list to fetch. We have shipped the browsing mechanics at marketplace scale, with multi-currency pricing and location-filtered feeds. #### We already sell through a website. Do we need an app? Not always, and we will say so when a well-built mobile web experience is the honest answer. An app earns its budget through the retention surface a website does not have: push notifications for offers and order updates, a home-screen icon, native payment sheets, and loyalty mechanics that reward returning. If most of your revenue is repeat customers, that case is usually strong; if it is one-off search traffic, spend on the web first — we build that too. #### Can one codebase power several branded shop apps? Yes — that is our most direct commerce credential. One Flutter codebase we built produces more than fifteen fully branded consumer apps plus a merchant app, with an admin panel that launches a new brand without engineering work. The store-policy trap is Apple guideline 4.2.6, which exists to reject fleets of near-identical apps, and a white-label platform has to be designed to pass it from the start. The full architecture is written up on our white-label service page and in the accompanying engineering post. Read the cost breakdown in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026), see [how the loyalty-platform fleet is built](https://nerdy.pro/blog/white-label-app-platform-flutter), or start from the [Flutter app development](https://nerdy.pro/services/flutter-app-development) pillar. ## Embeddable Widgets & Scripts https://nerdy.pro/services/web-development/embeddable-widgets An embeddable widget is code your customers paste into their own websites: an analytics snippet, a chat bubble, a booking form, a measurement collector. It is the hardest kind of frontend engineering because everything a normal web project takes for granted is gone: you do not choose the framework, the browser baseline, the other scripts on the page, or when your users upgrade. Your code runs as a guest on pages you will never see, and it has to behave on all of them at once. This is a niche discipline, and we have shipped in it: a signal collector we built runs embedded in thousands of sites we do not control, feeding a platform that scores tens of thousands of requests per minute. #### The products we build - **Measurement and analytics collectors** — scripts that gather signals from a page and ship them to your platform, like the one behind our bot-detection work - **Functional widgets** — chat, booking, forms, calculators: a piece of your product living inside your customer's page - **Snippet-installed integrations** — the "add one script tag" onboarding that puts your product in front of your customer's visitors - **The ingest behind the script** — the backend that receives what the snippet sends, at whatever volume your customers' traffic adds up to - **The dashboard your customers watch it from** — reporting and configuration surfaces are covered on [admin panels & dashboards](https://nerdy.pro/services/web-development/admin-dashboards) ### Who Builds Your Embeddable Script We built one of these end to end, from the snippet to the platform behind it. #### A collector in thousands of hostile pages For a [real-time bot detection platform](https://nerdy.pro/portfolio/realtime-bot-detection-platform), we built the browser-side collector that gathers the evidence a human-or-bot verdict is made from. It is plain JavaScript with no framework, no build-time runtime, and no dependencies, because it runs in thousands of pages alongside whatever else those pages load, cannot assume a module system or a polyfill set, and must never collide with the site around it. The environment is actively hostile: the things it measures try to defeat it. It is also small, because every kilobyte is charged against a customer's own performance budget, on every page view, forever. #### And the platform that receives what it sends The snippet is the small, visible part of the system. Behind that collector we built the ingest and scoring services in Go and the data platform underneath them, because an embed that succeeds multiplies its own traffic by every customer who installs it, and the backend has to be designed for that arithmetic from day one. The full architecture is in the [case study](https://nerdy.pro/portfolio/realtime-bot-detection-platform). ### What Embedded Code Actually Demands Five constraints separate this work from ordinary frontend engineering. #### A size budget measured in kilobytes Your script ships to every visitor of every customer. A framework runtime would be larger than a whole well-built collector, which is why serious embeds are dependency-free by design rather than by preference. Your customers can see the size of your script in their own Lighthouse scores. #### Zero assumptions about the host page No module system, no modern-browser baseline, no guarantee the page isn't already loading a conflicting library, an overzealous CSS reset, or a second copy of your own script. Everything runs in its own scope, touches the global namespace once if at all, and degrades silently rather than breaking someone else's checkout. #### Defensive by design The page your code runs in may be instrumented, patched, or emulated around it. In our bot-detection work, defeating the measurement is the adversary's whole job. Even in friendlier embeds the lesson holds: read browser APIs directly, verify rather than trust the environment, and keep every judgment server-side where the logic is not visible to the page. #### Versioning you cannot force Customers paste your snippet once and never touch it again. That one line has to stay a stable loader while everything behind it evolves: staged rollouts, an unbreakable contract between loader and payload, and compatibility maintained for years, because "please update your embed code" is an email nobody answers. #### Consent and privacy as architecture A third-party script is a third party, so GDPR applies on every site that installs it. What the script collects, when it may run relative to the host page's consent state, and what crosses the wire are design decisions we make explicit up front, so your customers' own compliance reviews can wave the embed through rather than flag it. ### How We Work **Fixed-scope build.** Snippet, loader, ingest, and the deployment pipeline that versions them safely, scoped and shipped as one system. **Staff augmentation.** Our engineers join the team that owns your platform — see [team augmentation](https://nerdy.pro/services/team-augmentation). Either way, we send a scoped quote within two business days of understanding the requirements. ### What an Embeddable Widget Costs The snippet is small; the system is not. An embed is priced from the same published tiers as the [web development](https://nerdy.pro/services/web-development) page. The collector or widget together with its loader and a lean ingest sits in the web app tier at $15,000 – $40,000; when the platform behind it carries real scale, with scoring, analytics, and customer dashboards, the system prices as SaaS-grade work from $50,000+. These are the same keys that render the pillar's pricing table, so the two cannot drift. What moves the number: how hostile the environments your script must survive, how much traffic your customers' pages add up to, and how much of the receiving platform already exists. ### Frequently Asked Questions Common questions from teams shipping code into other people's pages. #### Why can't we build our widget with React or Vue? Because the framework runtime would be larger than the entire widget, and because the host page may already be running a different version of the same framework, or a global environment that fights yours. An embed that ships to every visitor of every customer is measured against each customer's own performance budget, so serious embedded scripts are dependency-free vanilla JavaScript by design. Your product's own site can be as framework-heavy as it likes; the code that travels must be self-contained. #### How does an embeddable script avoid breaking the sites that install it? By assuming nothing and touching nothing. Everything runs inside its own scope, the global namespace is touched once if at all, styles are isolated so the host page's CSS cannot bleed in or out, and every failure path degrades silently, because a broken widget must never take a customer's checkout down with it. We build against hostile-page conditions from the start, because the collector we operate runs alongside whatever thousands of unrelated sites happen to load. #### How do you update a script that customers pasted years ago? The pasted line is never the product. It is a tiny, stable loader that fetches the current payload. The loader-payload contract is the one interface that must never break, and everything behind it can then evolve: staged rollouts to a fraction of traffic first, versioned payloads, and instant rollback when a browser release surprises you. Designing that seam properly on day one is what makes 'please update your embed code' an email you never have to send. #### What about GDPR and cookie consent for embedded scripts? A third-party script is a third party under GDPR on every site that installs it, so we design for consent from the start. We make explicit what the script collects, whether any of it identifies a person, and how it behaves relative to the host page's consent state — including running in a no-consent mode where that is what compliance requires. The goal is an embed your customers' own privacy reviews can approve without a meeting. #### Can you also build the backend that receives the data? Yes, and we would rather build both sides than either alone. A successful embed multiplies its traffic by every customer who installs it, so the ingest has to be designed for aggregate volume from the start. The platform behind our collector scores tens of thousands of requests per minute in real time, on Go services we built end to end. The snippet and its backend are one system, and the failure modes live at their seam. #### How much does an embeddable widget cost to build? A widget or collector with its loader and a lean ingest typically runs 15,000 to 40,000 US dollars over one to three months, the published web app tier on our web development page. When the receiving platform is the real product, with real-time scoring, analytics, and customer-facing dashboards, the system prices as SaaS-grade work upward of 50,000. The snippet itself is rarely the expensive part; the volume it generates is. Read how the collector fits the whole system in the [bot-detection case study](https://nerdy.pro/portfolio/realtime-bot-detection-platform), see the [Go services](https://nerdy.pro/technologies/go) behind it, or start from the [web development](https://nerdy.pro/services/web-development) pillar. ## Fintech App Development https://nerdy.pro/services/flutter-app-development/fintech Fintech app development is the design and engineering of mobile applications that move, display, or manage money — investing and trading apps, neobanks, payment and wallet products, and insurtech. It differs from ordinary app development in three specific ways: the data changes while the user is watching it, the security bar is set by regulators rather than by your team, and an arithmetic error is a financial loss rather than a cosmetic bug. Flutter is suitable for fintech because it renders its own UI. A custom chart, a portfolio dashboard, or a transaction list behaves identically on iOS and Android from a single codebase, at roughly 30–40% less than building the same product natively twice. The parts that touch the platform directly — hardware-backed key storage, payment sheets, some KYC SDKs — run through platform channels, which is routine work but real work. #### The products we build - **Investing and trading apps** — portfolios, watchlists, order flow, and chart-heavy screens that stay smooth while quotes move - **Neobanks and banking clients** — accounts, transfers, statements, card controls, and onboarding with document capture - **Payment and wallet products** — checkout, transaction history, recurring payments, and native payment sheet integration - **Personal finance and cashback** — aggregation, categorisation, and reward mechanics, as in the [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform) we built - **Insurtech** — quote flows, claims capture, and policy management, where the document and photo pipeline usually matters more than the dashboard ### Who Builds Your Fintech App Two things separate a team that can build a finance app from one that has only built apps. #### Payments infrastructure, operated at scale Nerdy Production is led by [Ilya Nixan](https://nerdy.pro/team/nixan), previously CTO of QIWI, one of the largest payment platforms in its market, where he ran roughly 12 engineering teams covering everything from web products down to card processing, PCI-DSS scope, and contactless payments. He built contactless card payments on Android using Host Card Emulation over ISO/IEC 14443, with EMV Contactless (Visa PayWave) on top — shipping NFC card payments about a year before Google's own Android Pay reached the market. He was also the architect of the Evotor point-of-sale terminal, a forked-:term{slug="aosp"} device running cashier workloads in production. What that means for you: the person leading your build has operated regulated payments infrastructure, carried PCI-DSS scope, and shipped card-present payment flows — not just written apps that call a payments API. #### A fintech product already live on both stores We built [ExtraETF](https://nerdy.pro/portfolio/extraetf) for Isarvest GmbH in 2022 — an investing app on Flutter with a Go backend, delivering real-time market data, interactive charts, and portfolio tracking, live on the App Store and Google Play. The engineering write-up is in [How We Built a Real-Time Fintech App with Flutter and Go](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go), including the WebSocket fan-out that keeps quotes flowing without overwhelming the client. ### What Fintech Apps Actually Demand Five concerns come up in every finance build. Here is how we handle each. #### Real-time market and transaction data Prices, balances, and order states change continuously, and the UI has to absorb that without dropping frames or losing the user's scroll position. On ExtraETF the answer was a Go service fanning out market data over WebSockets, paired with a client that scopes rebuilds to the series that actually changed. The naive implementation — rebuilding a widget subtree on every tick — is what makes chart screens stutter on mid-range Android hardware. The decisions that matter here are made on the transport, not in the widget tree: how often the server is allowed to push, whether updates are coalesced into a window, and what the client does when the socket drops mid-session and reconnects to a different price than the one on screen. Getting reconnection wrong is the bug users actually notice, because it shows them a number that was true thirty seconds ago. #### Security and compliance We do not hold PCI-DSS or SOC 2 certification, and any agency that implies otherwise is selling you something. What we do is build so that your certification effort is achievable rather than a rewrite: - Credentials in the iOS Keychain and Android Keystore, never in shared preferences - TLS with certificate pinning, and a rotation plan that does not brick older clients - Biometric and passcode gating on sensitive actions, not just at app launch - Encrypted local storage for anything cached on device - No personal or financial data in logs, crash reports, or analytics payloads - Card data kept out of your app's scope wherever the provider's SDK allows it The architectural constraints imposed by PCI-DSS, GDPR, SOC 2, and KYC/AML are decisions you make early or pay for later — which is exactly the experience our founder carried out of card processing. Practically, that means deciding up front which data your app is allowed to touch at all: card entry handed to the provider's SDK so raw PAN never reaches your code, personal data segregated so a GDPR deletion request is a query rather than an archaeology project, and an audit trail designed alongside the feature rather than retrofitted when someone asks for one. Our [guide to encryption in mobile apps](https://nerdy.pro/blog/encryption-explained) covers the architecture we hold ourselves to, including where it quietly breaks. #### Precise money math Money is never a floating-point number. In IEEE 754, `0.1 + 0.2` does not equal `0.3`, and in a finance app that rounding error compounds into a failed reconciliation. We represent amounts as integers in minor units, or with a decimal type, and keep rounding rules explicit and tested at every boundary where money is displayed, transmitted, or stored. The long version is in [Why 0.1 + 0.2 ≠ 0.3](https://nerdy.pro/blog/floating-point-and-money). #### Payments, subscriptions, and entitlements Payment gateway integration, in-app purchase, subscription tiers, and entitlement checks are where finance apps accumulate edge cases: restoring purchases on a new device, a subscription that lapses mid-session, a payment that succeeds at the provider but fails to confirm to your backend. We keep entitlement state owned by the server and merely reflected in the client, because the alternative is a user who paid and cannot access what they bought. There is also a store-policy dimension that catches teams out. Apple and Google require their own in-app purchase for digital goods, while genuine financial services — moving your own money, buying securities — go through a payment provider instead. Which side of that line your product sits on changes both the integration and the review outcome, and it is worth settling before the feature is built rather than at submission. #### Performance and reliability Finance users have a low tolerance for a spinner where a balance should be. That means chart-heavy screens holding 60fps on real devices rather than flagship simulators, offline-first behaviour so cached data renders instantly while fresh data loads, and failure states that say what happened instead of silently showing a stale number. We treat "when was this number last true?" as a first-class piece of UI state, and we test on the mid-range Android hardware your users actually carry. ### Why Flutter for Fintech The honest case for Flutter here: | Requirement | How Flutter handles it | | --- | --- | | iOS and Android from one team | One codebase, roughly 30–40% less than two native builds | | Custom finance UI — charts, dashboards | Flutter draws its own pixels, so complex UI is identical on both platforms | | Smooth rendering under live data | Compiles to native code; 60fps is achievable with disciplined rebuild scoping | | Platform security primitives | Keychain, Keystore, and biometrics reached through platform channels | | Vendor SDKs for KYC and payments | Many ship Flutter packages; native-only ones need a platform-channel wrapper | The saving is not a discount on engineering — it is the removal of duplicated work: one set of business rules, one set of money-formatting logic, one set of screens, tested twice rather than built twice. Where native still wins: if your product is essentially a thin wrapper around a platform-specific capability — a deep Apple Wallet integration, or a card-present NFC flow tied to one platform's stack — the cross-platform advantage shrinks, and native may be the honest answer. We will tell you when that is the case. The general service page is [Flutter app development](https://nerdy.pro/services/flutter-app-development). ### How We Work **Fixed-scope build.** We own delivery end to end — discovery, architecture, build, store submission. Best when you want a defined scope handed off and a team accountable for the outcome. **Staff augmentation.** Our engineers join your existing team, in your repo and your sprints, under your management. Best when you already have engineering leadership and need Flutter capacity — see [team augmentation](https://nerdy.pro/services/team-augmentation) and our [guide to hiring Flutter developers](https://nerdy.pro/blog/hire-flutter-developers-2026), which is honest about when not to use us. Either way, we send a scoped quote within two business days of understanding the requirements. ### What a Fintech App Costs A focused first release — accounts, core transactions, and one or two headline screens — is a Business-tier build: $30–60K, over three to five months. A brokerage or banking client with live market data, KYC onboarding, and payment provider integrations is enterprise work, running from $90K+. These are the same published tiers as on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page — a vertical does not get a second price list. What moves a fintech number within them is rarely the UI: it is the count and maturity of third-party integrations, the compliance posture your product needs, and how much of the backend is yours versus a provider's. All three surface during scoping, which is why the quote comes after the questions rather than before them. ### Frequently Asked Questions Common questions from teams building financial products on Flutter. #### Is Flutter good for fintech apps? Yes. Flutter is well suited to fintech apps because it renders its own UI, which means custom finance interfaces — charts, portfolio dashboards, transaction lists — behave identically on iOS and Android from a single codebase, typically at 30 to 40 percent less than building two native apps. It compiles to native code, so performance under live market data is achievable. The trade-off is that platform-level capabilities such as hardware-backed key storage, payment sheets, and some KYC SDKs are reached through platform channels rather than directly, which is routine but real engineering work. #### Is Flutter secure enough for banking apps? Yes, and it is already used in production banking and investing apps. Flutter does not change your security posture, because it uses the same platform primitives a native app uses: the iOS Keychain, the Android Keystore, biometric APIs, and certificate pinning, all reached through platform channels. What determines whether a finance app is secure is implementation discipline, not the UI framework. The failures we see in practice are credentials in insecure storage, personal data written to logs, and missing certificate pinning, and all three appear just as often in native codebases. #### Can Flutter handle real-time market data? Yes, and it is the part that most rewards deliberate engineering. We built ExtraETF, a live investing app, on Flutter with a Go backend that fans out market data over WebSockets. The common failure mode is rebuilding large widget subtrees on every price tick, which drops frames on mid-range devices. The fix is scoping rebuilds to the data that actually changed and choosing the right update granularity on the transport, rather than pushing every tick straight into the widget tree. #### How do you handle money calculations in a Flutter app? Never with floating-point numbers. In IEEE 754 floating point, 0.1 plus 0.2 does not equal 0.3, and in a financial application that rounding error compounds into failed reconciliation. We represent monetary amounts as integers in minor units, cents rather than euros, or use a decimal type, and we make rounding rules explicit and tested at every boundary where money is displayed, transmitted, or stored. This is a correctness requirement, not a preference. #### How much does it cost to build a fintech app? A focused first release with accounts, core transactions, and one or two headline screens typically runs three to five months of build. A brokerage or banking client with live market data, KYC onboarding, and payment provider integrations runs longer, because the timeline is driven by third-party integrations and review cycles more than by UI work. Our breakdown of how scope, backend, integrations, and compliance compound into a final number is in our guide to Flutter app development cost. #### Can Flutter apps handle payments and subscriptions? Yes. Flutter apps integrate payment gateways, native in-app purchase for digital goods, and subscription tiers with entitlement checks. The engineering difficulty is not the happy path but the edge cases: restoring purchases on a new device, a subscription that lapses mid-session, and payments that succeed at the provider but fail to confirm to your backend. We keep entitlement state owned by the server and merely reflected in the client, so a user who has paid is never locked out by a client-side assumption. #### Do you have PCI-DSS or SOC 2 certification? No, and you should be sceptical of any app agency that offers a certification as a selling point rather than describing how they build. Certification applies to the organisation handling the data, which for most fintech products is you and your payment provider. What we bring is architecture that keeps card data out of your app's scope where the provider allows it, plus a founder who carried PCI-DSS scope and ran card processing at a major payment platform, so the constraints are designed in from the start rather than discovered during an audit. Read the cost breakdown in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026), or see the [ExtraETF case study](https://nerdy.pro/portfolio/extraetf) for what a shipped Flutter fintech product looks like. ## Flutter App Development https://nerdy.pro/services/flutter-app-development Flutter app development means building your iOS and Android apps — and, where it earns its place, your web app — from one codebase, with one team, on Google's Flutter toolkit. Done well, it costs **30–40% less to build than two native apps**, and the gap widens over the product's life, because every future feature and fix is written once instead of twice. Done badly, it produces an app that feels slightly wrong on both platforms — which is why the framework matters less than who is holding it. We are a team that has been shipping Flutter to production since 2019 and does very little else. Seven of the eight projects in our [portfolio](https://nerdy.pro/portfolio) are Flutter apps, from a live investing platform to a fleet of fifteen white-label apps built from a single codebase. Our engineers maintain [open-source Flutter packages](https://nerdy.pro/open-source) on pub.dev, and our prices are published on this page rather than quoted privately — we would rather put the numbers up and be argued with than open with "it depends". The full breakdown of what drives them is in our [cost guide](https://nerdy.pro/blog/flutter-app-development-cost-2026). - **30-40%** Lower Build Cost: One codebase instead of two native apps — and the gap widens with maintenance - **2-3x** Faster to Both Stores: One team ships iOS and Android together - **7** Flutter Apps Shipped: In production, with case studies you can read - **6** Open-Source Packages: Maintained by our engineers on pub.dev and beyond ### Who Builds Your Flutter App Anyone can list frameworks. What separates a Flutter shop from a shop that has used Flutter is production work you can inspect — so here is ours. #### A live investing app under real-time market data [ExtraETF](https://nerdy.pro/portfolio/extraetf) is an investing app for Isarvest GmbH: live market prices, portfolio tracking, and charts that stay smooth while quotes stream in over WebSockets from a Go backend we also built. Six months to both stores. The hard part of an app like this is not drawing the chart — it is keeping the frame rate flat while data arrives every few hundred milliseconds, which is a rebuild-scoping problem we have written up in [engineering detail](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go). #### Two native apps, merged into one codebase [Formtastic](https://nerdy.pro/portfolio/formtastic) shipped for years as separate iOS and Android apps — every feature built twice, releases drifting out of sync. We consolidated the product into a single Flutter codebase without disrupting the customers already depending on it, then kept building: iPad support, offline sync, voice-to-text for form fields. It is the clearest before-and-after we can show for what a single codebase does to a roadmap, and the client says so below in his own words. #### Fifteen branded apps from one codebase For a cashback and loyalty platform under NDA, one Flutter codebase produces [a fleet of more than fifteen fully branded apps](https://nerdy.pro/portfolio/cashback-loyalty-platform), each with its own name, theme, and store listing, plus a merchant app and an admin panel that launches a new brand without engineering work. That build became its own service line — [white-label app development](https://nerdy.pro/services/whitelabel-app-development) — and its own [written-up architecture](https://nerdy.pro/blog/white-label-app-platform-flutter). Smaller and faster, same discipline: [Arcana](https://nerdy.pro/portfolio/arcana), an AI chat companion streaming markdown at 60 fps through thousands of messages, took three weeks. [OneTwoDo](https://nerdy.pro/portfolio/onetwodo), a two-sided services marketplace, took seven. [Jepta](https://nerdy.pro/portfolio/jepta), a neighborhood app with real-time chats and channels, shipped its full feature set inside six months. > For years, Formtastic shipped as two separate native apps — one for iOS, one for Android. Every feature had to be built twice, releases drifted out of sync between the platforms, and maintaining two codebases was quietly draining our roadmap. > > Nerdy Production consolidated everything into a single Flutter codebase without disrupting the product our customers depend on. Now both apps ship the same features at the same time, the experience is identical on iPhone and Android, and our time to production has dropped dramatically. We maintain one codebase instead of two, and the team finally spends its energy on new features rather than keeping two apps in sync. *[Julian Garbotz](https://www.linkedin.com/in/julian-garbotz), Managing director, Formtastic GmbH* ### How Flutter Works: Where Its Performance Comes From The reason Flutter apps do not carry the "hybrid app" performance penalty is architectural, and worth two minutes even if you never read Dart. #### Flutter's Rendering Architecture 1. Direct Compilation to Native Code Unlike hybrid frameworks that run through JavaScript bridges, Flutter compiles ahead of time to native ARM machine code. There is no interpreter between your app and the processor, so startup times and execution speed sit in the same range as apps written in Swift or Kotlin. 2. Its Own Graphics Engine Flutter draws every pixel itself — through Skia, the 2D engine behind Chrome and Android, and its successor Impeller on modern devices. Instead of assembling platform UI components, it renders the interface directly to the screen canvas. That is why a Flutter app looks and behaves identically on every platform, and why complete control over every animation is the default rather than a fight. 3. Declarative UI Framework Flutter's widget tree rebuilds only the parts of the UI that change, using an efficient diffing algorithm similar to React's. Complex screens with hundreds of animated elements hold 60 fps — 120 fps on displays that support it — provided rebuilds are scoped with care, which is precisely the kind of care that separates production Flutter work from demo Flutter work. 4. Optimized Layer Architecture The engine (C/C++) handles rendering, text layout, and I/O; the framework (Dart) provides the API on top. The separation keeps performance-critical operations at native speed while the code your team works in stays productive and testable. #### Why Performance Isn't a Concern - 60/120 FPS Animations: Smooth animations on all platforms without frame drops - Fast Startup Times: Apps launch in milliseconds, like native applications - Small Binary Size: Optimized code splitting and tree shaking reduce app size - Low Memory Footprint: Efficient memory management keeps apps responsive ### Flutter vs React Native vs Native Development See how Flutter compares to other mobile development approaches | Feature | Flutter (Recommended) | React Native | Native (Swift/Kotlin) | | --- | --- | --- | --- | | Performance | Native Speed | Near Native | Native | | Development Speed | Very Fast | Fast | Slow (2x codebase) | | UI Consistency | Pixel Perfect | Platform Dependent | Requires 2x Design | | Code Reusability | 90-95% | 70-85% | 0% (separate apps) | | Hot Reload | Sub-second | Available | Limited | | Platform Support | 6 Platforms | iOS, Android, Web | One Platform Only | | Learning Curve | Easy (Dart) | Easy (JavaScript) | Steep (2 languages) | | Development Cost | 30-40% Lower | 20-30% Lower | Highest (2x team) | | Maintenance | Single Codebase | Platform-specific Fixes | 2x Maintenance | | Backed By | Google | Meta | Apple / Google | > **Bottom line:** Flutter delivers native performance with significantly lower development and maintenance costs. React Native is a credible option — especially for a team already deep in React — but Flutter's rendering model, UI consistency, and platform reach make it the stronger default for most products. For the full trade-offs, read [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026) and [Flutter vs Native in 2026](https://nerdy.pro/blog/flutter-vs-native-2026). ### When Flutter Is the Wrong Choice A recommendation is only worth something if it can come out the other way, so here is where we advise against our own service. - **The product is a platform capability.** A deep Apple Watch experience, a widget-led product, an app built around an API that exists on one platform only — the cross-platform saving is not the point, and native is the honest answer. - **Your team is already a native team.** If you have strong Swift and Kotlin engineers, an established codebase, and no cost pressure from double delivery, switching frameworks solves a problem you do not have. The same honesty applies in reverse to [migrations](https://nerdy.pro/services/flutter-app-development/flutter-migration): we open that page by telling you who should not migrate. - **You want shared logic but native UI.** [Kotlin Multiplatform](https://nerdy.pro/technologies/kmp) shares business logic while keeping each platform's native interface. It is a different bet — more platform fidelity, less UI reuse — and for some teams it is the right one. We compare the two honestly in [Flutter vs Kotlin Multiplatform in 2026](https://nerdy.pro/blog/flutter-vs-kotlin-multiplatform-2026), and we will say so in the first call rather than the last. Everything else — consumer products, marketplaces, fintech, healthcare, internal tools, products that must exist on both stores with one budget — is the territory where Flutter, in our experience across seven production apps, wins on the merits. ### What We Build The pillar is one service; the work comes in shapes. Each of these pages publishes its own scope, numbers, and the honest failure modes: - **[MVP development](https://nerdy.pro/services/flutter-app-development/mvp)** — the smallest app that can answer your business question, on both stores in two to three months, at a published price. - **[Fintech apps](https://nerdy.pro/services/flutter-app-development/fintech)** — investing, banking, and payments products: real-time market data, precise money arithmetic, compliance posture. ExtraETF is the proof. - **[Healthcare apps](https://nerdy.pro/services/flutter-app-development/healthcare)** — telemedicine and mental-health products: video consultations that survive real networks, patient data handled under HIPAA and GDPR discipline. - **[E-commerce apps](https://nerdy.pro/services/flutter-app-development/e-commerce)** — catalog, checkout, and payments, from a single storefront to a branded fleet; the tier in the pricing table below finally has its own page. - **[Marketplace apps](https://nerdy.pro/services/flutter-app-development/marketplace)** — two-sided products, both roles from one codebase. OneTwoDo is the proof. - **[Social & community apps](https://nerdy.pro/services/flutter-app-development/social)** — chats, channels, and live feeds on one real-time layer. Jepta is the proof. - **[React Native and native migration](https://nerdy.pro/services/flutter-app-development/flutter-migration)** — screen-by-screen, add-to-app, no downtime, and a page that opens by telling you when not to do it. - **[Add-to-app integration](https://nerdy.pro/services/flutter-app-development/add-to-app)** — Flutter embedded inside the native app you already have, shipping new features on both platforms without committing to a migration. - **[App rescue & performance](https://nerdy.pro/services/flutter-app-development/app-rescue)** — audits, frame-rate work, and stabilisation for a Flutter app failing in production, scoped from the symptoms rather than a feature list. - **[White-label platforms](https://nerdy.pro/services/whitelabel-app-development)** — one codebase, a fleet of branded apps, and the Apple guideline 4.2.6 traps designed out from the start. - **[Chatbot to app](https://nerdy.pro/services/chatbot-to-app)** — a Telegram or WhatsApp bot with a real user base, turned into a store app without losing them. - **[Team augmentation](https://nerdy.pro/services/team-augmentation)** — senior Flutter engineers embedded in your team, in your repo and your sprints, when you need capacity rather than delivery. If the app already exists and the question is its condition rather than its roadmap — an agency handoff, an AI-built prototype, a codebase you have doubts about — start with an [AI code audit](https://nerdy.pro/services/ai-code-audit) instead. ### How We Work **Fixed-scope delivery.** We own the outcome end to end: discovery, design, architecture, build, store submission. One team accountable for the launch date. What that looks like week to week is in our [development process write-up](https://nerdy.pro/blog/flutter-app-development-process), and the phase-by-phase timeline in [How Long Does It Take to Build a Flutter App?](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) **Staff augmentation.** Our engineers join yours, under your management. Best when you already have engineering leadership and need Flutter depth — our [guide to hiring Flutter developers](https://nerdy.pro/blog/hire-flutter-developers-2026) is honest about when this beats an agency engagement and when it does not. Whichever route fits, we send a scoped quote within two business days of understanding the requirements — and the engineering we refuse to cut regardless of budget is the same everywhere: CI/CD from week one, tests where being wrong costs money, and module boundaries that let v2 grow without a rewrite. ### Flutter App Development Pricing Transparent pricing for your Flutter app project #### MVP Validate your idea quickly **$15–30K** 2-3 months - iOS & Android apps - 5-8 core features - Basic UI/UX design - REST API integration - User authentication - Basic analytics - App store deployment [Get Started](https://nerdy.pro/contact?intent=flutter-app-development) #### Business App Full-featured solution **$30–60K** 3-5 months - iOS, Android & Web - 15-20 features - Custom UI/UX design - Advanced API integrations - Push notifications - Offline functionality - Analytics & monitoring - Admin dashboard [Get Started](https://nerdy.pro/contact?intent=flutter-app-development) #### E-commerce / Retail (Popular) Sell online with style **$50–90K** 4-6 months - All platforms + Desktop - Product catalog & search - Shopping cart & checkout - Payment gateway integration - Order tracking - Customer reviews & ratings - Inventory management - Admin & vendor panels [Get Started](https://nerdy.pro/contact?intent=flutter-app-development) #### Enterprise / AI Complex & scalable **$90K+** 6+ months - All platforms - AI/ML integration - Complex workflows - Enterprise security - Custom backend infrastructure - Advanced analytics - Multi-tenant architecture - Dedicated support [Get Started](https://nerdy.pro/contact?intent=flutter-app-development) > **Custom Pricing Available** > > Every project is unique. These prices are approximate ranges based on typical project requirements. We'll provide a detailed quote after understanding your specific needs, timeline, and complexity. Contact us for a free consultation and accurate pricing estimate. For what actually drives app cost, see [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026) and our [mobile cost-reduction case studies](https://nerdy.pro/blog/mobile-cost-reduction-case-studies). ### Frequently Asked Questions Everything you need to know about our Flutter app development services #### What is Flutter and why should I use it for my app? Flutter is an open-source UI toolkit from Google for building natively compiled applications for mobile, web, and desktop from a single codebase. The business case is structural: one codebase means one team, one release train, and every feature built once — which is why a Flutter build typically costs 30 to 40 percent less than two native apps, with the gap widening over the product's life as maintenance compounds. The engineering case is that Flutter compiles to native code and draws its own UI, so it avoids both the performance penalty of hybrid frameworks and the platform drift of maintaining two implementations. #### How much does Flutter app development cost? Our published tiers run from $15,000 to $30,000 for an MVP, $30,000 to $60,000 for a full business app, $50,000 to $90,000 for an e-commerce product, and from $90,000 for enterprise work — the exact ranges are in the pricing section of this page rather than behind a sales call. What moves the number is scope first and integrations second. The full breakdown of what drives the figure, including the costs teams forget to budget, is in our guide to Flutter app development cost in 2026. #### How long does it take to build a Flutter app? An MVP takes two to three months from kickoff to a live store listing. A full business app runs three to five months, and an e-commerce or enterprise build four to six or more. The calendar is rarely consumed by the code: payment flows that appear mid-project, design rounds, third-party API access, and the store submission cycle are where timelines actually move. Our phase-by-phase breakdown is in How Long Does It Take to Build a Flutter App. #### Can Flutter apps perform as well as native apps? Yes. Flutter compiles ahead of time to native ARM code and renders through its own graphics engine, so animations hold 60 frames per second — 120 on supported displays — without the bridge overhead hybrid frameworks carry. Production apps like Google Ads, Alibaba, and BMW's vehicle app run on Flutter, and our own ExtraETF holds its frame rate under live streaming market data. The honest caveat is that performance is a property of the engineering, not the framework: unscoped widget rebuilds will drop frames in Flutter exactly as leaked observers will in Swift. #### How is Flutter different from React Native? Flutter compiles to native code and draws its own UI through its rendering engine; React Native bridges to each platform's native components. In practice that gives Flutter better performance headroom, pixel-identical UI across platforms, 90 to 95 percent code reuse against 70 to 85 for React Native, and six supported platforms including desktop. React Native remains a credible choice for teams already deep in React. Our full comparison, including where React Native wins, is in Flutter vs React Native in 2026. #### Is Flutter a good choice for web and desktop apps? With qualifications. Flutter web is strongest for app-like experiences — dashboards, portals, tools behind a login — where its canvas rendering is an asset rather than a cost; for content sites that live on SEO and instant first paint, a conventional web stack is the better tool, and we say so even though we sell both. Desktop support for Windows, macOS, and Linux is production-ready and shines for internal tools and companion apps sharing a mobile codebase. #### Can you take over or rescue an existing Flutter app? Yes — some of our best-documented work started that way, including a live telehealth product we stabilised while it stayed in production. A takeover starts with an audit of the codebase: architecture, state management, test coverage, and the specific symptoms that prompted the call. You get a written findings report and a sequenced plan before any rebuild conversation happens, because sometimes the answer is two weeks of fixes rather than a rewrite — and when it is, we say so. #### Do you build the backend as well as the app? Yes. ExtraETF runs on a Go backend we built for real-time market data fan-out; Formtastic's platform includes backend and web alongside the apps; and for MVPs we deliberately reach for managed backends like Firebase when writing one from scratch would be spending your budget on plumbing. The choice between custom and managed is an engineering decision we make openly during scoping, because it moves both the price and the timeline. #### Will Flutter still be a safe bet in five years? As safe as bets get in this industry. Flutter is developed in the open by Google, ships regular stable releases, powers Google's own revenue-critical apps like Google Ads, and has one of the largest cross-platform communities and package ecosystems. No framework choice is risk-free — which is why the more durable insurance is code quality: a well-structured Flutter app with clear module boundaries keeps its business logic portable no matter what the industry does next. #### Which industries do you build Flutter apps for? Our deepest vertical work is in fintech — investing, banking, and payments products — and healthcare, including telemedicine, each with its own dedicated service page covering the compliance and engineering specifics. Beyond those we have shipped marketplaces, social and community products, loyalty platforms, and AI-driven consumer apps. What transfers across all of them is the production discipline: real-time data handling, offline behaviour, accessibility, and store review requirements. #### How do we start a project with you? Tell us what you are building through the contact form, including whatever exists today — a spec, a prototype, an app already in the stores. We come back with questions, not a pitch deck, and once we understand the requirements you get a scoped quote within two business days: what is in, what is explicitly out, what it costs, and how long it takes. If Flutter is the wrong tool for your product, that conversation is where we tell you. Start with the numbers: [what a Flutter app costs in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026), [how long it takes](https://nerdy.pro/blog/how-long-to-build-a-flutter-app), and [what maintenance runs after launch](https://nerdy.pro/blog/flutter-app-maintenance-cost). Or read the work: the [ExtraETF](https://nerdy.pro/portfolio/extraetf) and [Formtastic](https://nerdy.pro/portfolio/formtastic) case studies are the closest thing to seeing us mid-project. ## Flutter App Rescue & Performance Optimization https://nerdy.pro/services/flutter-app-development/app-rescue A rescue is the engagement that starts from symptoms instead of a feature list. The app already exists, it is live, and something about it is failing: frames drop, sessions disconnect, crashes climb, releases take longer every cycle — or the team that built it has stopped answering. Rescue work is scoped against those symptoms, sequenced by severity, and delivered into a product that stays in production the whole time. It is different work from building an app, and the evidence that matters is different. Anyone can promise a rebuild; a rescue is judged on whether the specific thing that is failing stops failing, measurably, without breaking what still works. That is why every rescue we run starts with an audit and a written plan — and why, when the plan says two weeks of fixes rather than a rewrite, we say so. #### The calls we take - **Performance** — jank on scroll, animation stutter, slow startup, lists that choke on real data volumes - **Stability** — rising crash rates, dropped sessions, memory leaks that surface only after minutes of real use - **Abandoned handoffs** — the agency is gone, the repo is unclear, and nobody is sure where the signing keys are - **AI-built codebases** — a generated prototype that found real users and now needs to survive them; the [AI code audit](https://nerdy.pro/services/ai-code-audit) is the front door for these - **Stuck roadmaps** — an architecture where every feature ships slower than the last, which is a symptom as real as a crash ### The Rescues Behind This Page #### A live telehealth product, stabilised in production [YouMi](https://nerdy.pro/portfolio/youmi) is an online psychological counselling service whose Flutter app was live on both stores and failing at the thing it existed to do: sessions dropped, chat messages went undelivered, and the navigation stack leaked memory badly enough to crash between screens. In two weeks we rebuilt the WebSocket layer with a real connection lifecycle — automatic reconnection, message queueing across network transitions — added full delivery-state tracking, and rewrote the navigation architecture on Flutter's modern routing APIs. The app never left the stores while it happened. In telehealth the connection *is* the product, which is why that engagement leads this page: a rescue is measured against what the product exists to do. #### Performance proven at the unreasonable end Fixing slow rendering requires having built fast rendering. [Arcana](https://nerdy.pro/portfolio/arcana) holds a chat of thousands of streaming markdown messages at 60 fps; [ExtraETF](https://nerdy.pro/portfolio/extraetf) holds its frame rate under live streaming market data. Those ceilings matter for rescue work because they calibrate the diagnosis: when we profile your app, we know the difference between "Flutter cannot do this" — rarely true — and "this rebuild storm can be scoped away", which is usually the actual finding. ### How a Rescue Runs #### 1. Audit We review the codebase — architecture, state management, test coverage, dependency health — against the specific symptoms that prompted the call, and profile the app on real devices rather than trusting the simulator. You get a written findings report and a sequenced plan. Sometimes that plan is two weeks of fixes; sometimes it is a staged refactor; occasionally it is an honest recommendation to rebuild, with the reasoning attached. #### 2. Stabilise The highest-severity symptoms are fixed first and shipped through staged rollouts, so relief reaches users in the first releases rather than at the end of the engagement. The app keeps its release cadence — freezing it would add an outage of the roadmap to the outage you already have. #### 3. Harden Fixes get tests around them so the failure cannot quietly return, CI runs them on every change, and crash and performance monitoring is wired to catch regressions before reviews do. This is the difference between a rescue and a recurring appointment. #### 4. Hand over The engagement ends with the codebase in a state a team can own — yours, or ours under [team augmentation](https://nerdy.pro/services/team-augmentation) if you want the capacity to stay. The findings report, the plan, and the monitoring remain yours either way. ### The Disciplines a Rescue Draws On #### Frame-rate work Performance problems get profiled before they get fixed — on mid-range hardware, under real data, with the timeline open. The usual findings are unscoped rebuilds cascading through the widget tree, lists rebuilding rows they should recycle, and work done during build that belongs elsewhere. Performance is a property of the engineering, not the framework: the same discipline that keeps a chat streaming at 60 fps is what removes the stutter from a product screen. #### State-management refactors Many failing apps are failing structurally: state scattered across screens, implicit dependencies, one change rippling into five regressions. We refactor State management incrementally — screen by screen, behind tests, while the app ships — rather than declaring a rewrite. The goal is an architecture where the next feature is cheaper than the last one was, which is the roadmap symptom reversing. #### Real-time reliability Dropped sessions and undelivered messages are lifecycle bugs: connections without a reconnection policy, messages without delivery states, sockets that die silently when the network changes. The fix is a real connection lifecycle, built once and owned — the discipline the YouMi engagement demonstrates and the reason it took two weeks rather than a quarter. #### Memory and navigation Leaks that crash an app after ten minutes of use hide in retained screens, listeners that outlive their widgets, and navigation stacks that never release what they push. We rewrite navigation on Flutter's modern routing APIs where the architecture demands it — YouMi's did — and we verify the fix with memory profiles, not optimism. #### Abandoned-agency handoffs A takeover starts with archaeology: repository access, signing keys, store accounts, backend contracts nobody wrote down. We have run this recovery before and know its traps — keys that exist only on a former contractor's laptop, store listings owned by the wrong account — and the audit's first deliverable in these cases is simply an inventory of what you actually control. It is unglamorous work that has to be done before any engineering matters. #### When it really is a rewrite Some apps arrive beyond economic repair — an architecture fighting the framework, dependencies years stale, no tests to refactor against. When the audit says that, we say it, with the reasoning in writing, and the conversation becomes a [scoped rebuild](https://nerdy.pro/services/flutter-app-development) informed by everything the audit found. What we will not do is bill a rescue against a codebase we already know is a teardown. ### What a Rescue Costs The audit is a fixed-scope engagement: $5–15K, the same full-audit tier published on the [AI code audit](https://nerdy.pro/services/ai-code-audit) page — the machinery is identical whether the code was written by an agency, an in-house team, or a model. Stabilisation work is then scoped from the audit's sequenced plan, symptom by symptom, so you approve work in slices rather than committing blind. If you want the capacity to stay on afterwards, the published [team augmentation](https://nerdy.pro/services/team-augmentation) rate applies: $10–14K per senior engineer per month. We do not quote stabilisation prices before the audit — a number produced without seeing the codebase would be a guess wearing a suit, and most teams that arrive at a rescue have been burned by exactly that kind of quote once already. ### Frequently Asked Questions Common questions from teams whose Flutter app is failing in production. #### Can you take over a Flutter app another agency built? Yes — some of our best-documented work started exactly that way, including a live telehealth product we stabilised while it stayed in production. A takeover begins with an audit of the codebase and an inventory of what you actually control: repository access, signing keys, store accounts, backend documentation. You get a written findings report and a sequenced plan before any rebuild conversation happens, because sometimes the answer is two weeks of fixes rather than a rewrite — and when it is, we say so. #### How do you speed up a slow Flutter app? By profiling before changing anything — on mid-range hardware, under real data volumes, with the performance timeline open. The common causes are unscoped widget rebuilds cascading through the tree, lists rebuilding rows they should recycle, and heavy work running during build. Flutter itself is rarely the ceiling: the framework holds 60 frames per second through streaming, animation-dense screens when the rendering is disciplined, so the fix is almost always scoping the work correctly rather than fighting the framework. #### Do we need a full rewrite? Usually not, and the audit is what answers it honestly. Most failing apps have specific, fixable causes — a missing connection lifecycle, scattered state, leaking navigation — that a sequenced refactor addresses while the app keeps shipping. Occasionally a codebase genuinely is beyond economic repair, and then the audit says so in writing, with reasoning, and the conversation becomes a scoped rebuild. What you should not accept from anyone is a rewrite recommendation made before reading the code. #### What do you need from us to start? Repository access, a build that runs, and whatever exists of store accounts, signing keys, and backend documentation — plus the symptoms as you experience them, in plain language. Missing pieces are normal in rescue situations; part of the audit in handoff cases is establishing what you actually control and recovering what you can. A current production build and access to crash reporting, if you have it, shorten the diagnosis considerably. #### Will the app stay live during the rescue? Yes — that is a design constraint of the engagement, not a hope. Fixes ship through staged rollouts in severity order, so users feel relief in the first releases, and the app keeps its normal release cadence throughout. We stabilised a live telehealth product this way without it leaving the stores. A rescue that requires taking the product offline is a rebuild wearing the wrong name, and we would tell you that at the audit stage. #### Can you rescue an AI-generated Flutter app? Yes, and it is an increasingly common call: a generated prototype finds real users and then meets production, where security holes, runaway API costs, and architectural shortcuts surface quickly. Our AI code audit exists precisely for this codebase profile and runs the same way as any rescue audit — findings report, severity-sequenced plan, then fixes. Generated code is not unfixable; it just fails in patterns we have learned to look for first. Read the [YouMi case study](https://nerdy.pro/portfolio/youmi) for the rescue this page is built on, the [AI code audit](https://nerdy.pro/services/ai-code-audit) page for the audit machinery, or start from the [Flutter app development](https://nerdy.pro/services/flutter-app-development) pillar. ## Flutter Staff Augmentation https://nerdy.pro/services/team-augmentation ### What Flutter Staff Augmentation Is Flutter staff augmentation is a hiring model where vetted Flutter engineers join your existing team and work under your management — in your repository, your sprints, and your tools — while we handle their employment, benefits, and bench. You get senior Flutter capacity in weeks instead of the months an in-house hire takes, without the long-term liability of a full-time employee. It is built for teams that already have engineering leadership and just need more Flutter throughput — to hit a deadline, cover a skills gap, or scale up and down as the roadmap shifts. If you would rather hand off a defined product and have us own delivery end to end, that is a [Flutter app development](https://nerdy.pro/services/flutter-app-development) engagement instead. Not sure which model fits? We wrote the honest breakdown in [Hire Flutter Developers in 2026: In-House vs Agency vs Staff Augmentation](https://nerdy.pro/blog/hire-flutter-developers-2026). - **6** Flutter Engineers: Senior developers ready to embed in your team - **8+ yrs** Flutter Experience: Shipping Flutter since 2018, in software since 2010 - **2-4 wks** Typical Time-to-Onboard: From match to productive in your codebase - **4+ hrs** Daily Timezone Overlap: Overlap with EU and US-East working hours ### Engagement Models: Pick How Much You Want to Manage Staff augmentation is one of three ways to work with us. The right one depends on how much of the delivery you want to own. | Model | Staff Augmentation (Most Flexible) | Dedicated Team | Project Build | | --- | --- | --- | --- | | Who manages | You do — your leads, your process | Self-managed pod with our lead | We own delivery end to end | | Best fit | You have eng leadership, need capacity | You want an outcome, lack Flutter management | Defined scope and a fixed deadline | | Ramp time | Days to 2-4 weeks per engineer | 2-4 weeks to stand up the pod | 1-3 weeks to kickoff | | Flexibility | Scale up or down monthly | Resize the pod at milestones | Fixed to the agreed scope | | Pricing model | Per engineer, monthly | Per pod, monthly | Fixed project fee | > **When Augmentation Is NOT the Right Fit** > > Staff augmentation only works if you can manage engineers — someone to own architecture, review PRs, and set priorities. If you don't have that capacity, augmented engineers drift, and you'll get worse results than an agency would deliver. In that case, choose a [project build](https://nerdy.pro/services/flutter-app-development) and let us own the outcome. The honesty saves you money either way. ### How It Works From first call to a productive engineer in your codebase, the process is five steps. 1. **Share your needs and stack.** Tell us the role, seniority, your architecture, and the gap you're filling. 2. **We match vetted engineers.** We shortlist from our bench — only people who fit your stack and domain. 3. **You interview and approve.** You make the call; no one joins your team without your yes. 4. **Onboard in 2–4 weeks.** The engineer plugs into your repo, sprints, and tools, and starts shipping. 5. **Scale up or down as needed.** Add engineers for a crunch, release them when the work is done — on monthly terms. ### Why Our Flutter Engineers When someone joins your team, you're trusting their judgment on your codebase. We vet for the things that actually predict good engineers, not resume keywords. #### What Every Engineer We Place Has Proven - Real shipped apps: Live on the App Store and Google Play — fintech, real-time, and consumer apps, not slideware - Open-source footprint: Published pub.dev packages and contributions used by developers worldwide - Architecture and state-management fluency: Reasoned opinions on Riverpod, BLoC, and Provider — and when to use each - Testing and CI/CD discipline: Automated builds, signing, store deployment, feature flags, and staged rollouts You can see the kind of work our engineers ship across our [portfolio](https://nerdy.pro/portfolio) — the chart-heavy fintech UI of [ExtraETF](https://nerdy.pro/portfolio/extraetf) and the fully custom design system of [Arcana](https://nerdy.pro/portfolio/arcana) — and browse our [open-source Flutter packages](https://nerdy.pro/open-source). When you augment with us, you get engineers who have already hit the hard parts of production Flutter: platform channels, native SDK integration, and 60fps on low-end devices. Meet the people who'd join your team on our [team page](https://nerdy.pro/team). ### Rates Transparent, per-engineer monthly rates. We're an Eastern-Europe-rooted team, so our blended rates land in the **$50 – $95/hour** range — a senior Flutter engineer for a fraction of US in-house cost, with no recruiting fees and no long-term liability. #### Mid-Level Engineer Solid delivery on your stack **$7–10K** per engineer / month - 3-5 years of Flutter - Feature delivery in your sprints - Ships within your architecture - Code review and testing [Request a Developer](https://nerdy.pro/contact?intent=staff-augmentation) #### Senior Engineer (Most Requested) Owns features end to end **$10–14K** per engineer / month - 5+ years of Flutter - Architecture and technical decisions - Platform channels and native SDKs - CI/CD and release engineering - Mentors your team [Request a Developer](https://nerdy.pro/contact?intent=staff-augmentation) #### Lead / Architect Sets direction for a pod **$13–18K** per engineer / month - 8+ years, Flutter from the early days - System and app architecture - Leads a dedicated team - Hiring-bar code review [Request a Developer](https://nerdy.pro/contact?intent=staff-augmentation) > **Custom Quote in Two Business Days** > > These are typical monthly ranges for a full-time engineer embedded in your team — the exact rate depends on seniority, stack, and engagement length, and part-time or multi-engineer pods are available. For how augmentation compares to hiring in-house or an agency, see [Hire Flutter Developers in 2026](https://nerdy.pro/blog/hire-flutter-developers-2026) and our [cost breakdown](https://nerdy.pro/blog/flutter-app-development-cost-2026). [Tell us what you need](https://nerdy.pro/contact?intent=staff-augmentation) and we'll send a scoped quote. ### Frequently Asked Questions Everything you need to know about extending your team with our Flutter engineers. #### What is Flutter staff augmentation? Flutter staff augmentation is a model where you embed one or more vetted Flutter engineers into your existing team. They work in your repository, your sprints, and under your management, but they're employed and supplied by an external partner. It lets you add senior Flutter capacity in weeks — without the recruiting time, benefits overhead, or long-term commitment of a full-time hire. #### How is staff augmentation different from hiring an agency? With staff augmentation, the engineers join your team and you manage them day to day, so the code and context stay in your repository. With an agency project build, the agency manages delivery and hands you a finished product. Choose augmentation when you have engineering leadership and need capacity; choose an agency build when you want to hand off a defined scope entirely. #### How fast can a developer start? A vetted engineer from our bench can typically start within days and is productive in your codebase within two to four weeks. Because they already know Flutter, the ramp is your domain and architecture — not the language or tooling. That's far faster than the two to four months a new in-house hire usually takes. #### How much does it cost to hire a dedicated Flutter developer? A dedicated Flutter developer through staff augmentation typically costs $7,000–$18,000 per engineer per month depending on seniority, at our Eastern European blended rates of roughly $50–95 per hour. You pay a monthly rate with no recruiting fees and no long-term employment liability, and you can stop when the work is done. We send a scoped quote within two business days. #### Can I scale the team up or down? Yes — flexibility is the core of the model. You can add engineers for a crunch and release them when the work is done, on monthly terms, with no severance or recruiting cycle. This lets you match Flutter capacity to your roadmap instead of carrying fixed headcount. #### Who manages the developers? You do. Augmented engineers work under your leads, in your process and sprints, like any team member — which is why augmentation only works if you have engineering management capacity. If you'd rather someone else own delivery, a dedicated team with our lead or a full project build is the better fit. #### How do you vet your Flutter engineers? Every engineer we place has shipped real apps that are live on the App Store and Google Play, and most have a public pub.dev or open-source footprint. We check for architecture and state-management fluency, testing and CI/CD discipline, and platform-channel and native-SDK experience. You interview and approve every engineer before they join — no one is placed on your team without your yes. ## Healthcare App Development https://nerdy.pro/services/flutter-app-development/healthcare Healthcare app development is the design and engineering of mobile applications that carry clinical or personal health data — telemedicine and video consultation apps, mental health and therapy platforms, patient portals, remote monitoring, and wellness products. It differs from ordinary app development in three specific ways: the rules for handling the data are set by law rather than by your privacy policy, a dropped connection happens in the middle of someone's consultation rather than in the middle of a feed, and a meaningful share of your users are unwell, elderly, or reaching the app through assistive technology. Flutter is suitable for healthcare because it renders its own UI. A booking flow, a symptom tracker, or a consultation screen behaves identically on iOS and Android from a single codebase, at roughly 30–40% less than building the same product natively twice. The parts that touch the platform directly — Apple HealthKit and Android Health Connect, Bluetooth peripherals, camera and microphone permissions, some video SDKs — run through platform channels, which is routine work but real work. #### The products we build - **Telemedicine and video consultation** — scheduling, waiting rooms, live video and chat, session notes, and follow-up - **Mental health and therapy platforms** — specialist selection, recurring sessions, and messaging between appointments, as in [YouMi](https://nerdy.pro/portfolio/youmi) - **Patient portals and clinic companions** — appointments, results, prescriptions, documents, and reminders on top of an existing clinical system - **Remote monitoring and chronic-condition management** — readings from wearables and Bluetooth devices, trends, thresholds, and clinician alerting - **Wellness, fitness, and adherence** — habit and medication tracking, where the regulatory surface is smaller but the retention problem is harder - **Clinician-facing tools** — rota and rounds apps, secure messaging, and capture tools built for one hand and a short window of attention ### Who Builds Your Healthcare App Two things separate a team that can build a healthcare app from one that has only built apps. #### A live telehealth product we were called in to rescue [YouMi](https://nerdy.pro/portfolio/youmi) is an online psychological counselling service — patients choose a specialist, book and reschedule sessions, and message their psychologist between appointments. When YouMi LLC came to us, the Flutter app was live on both stores and failing at the thing it existed to do: sessions dropped, chat messages went undelivered, and the navigation stack leaked memory badly enough to crash between screens. In two weeks we rebuilt the WebSocket layer with a real connection lifecycle — automatic reconnection, message queueing across network transitions, and proper error handling — added full delivery-state tracking so both patient and psychologist can see when a message was sent, delivered, and read, and rewrote the navigation architecture on Flutter's modern routing APIs. That engagement is the reason we talk about session reliability first on this page. In telehealth the connection *is* the product: a consultation that drops at minute eleven is not a degraded experience — it is a clinical appointment that did not happen, and it costs you the refund, the rebooking, and the patient. #### Real-time and secure messaging, built more than once The same problem shows up across our work. [Jepta](https://nerdy.pro/portfolio/jepta) is a hyperlocal communication app for German neighbourhoods with a WebSocket layer behind every chat and channel; [Arcana](https://nerdy.pro/portfolio/arcana) streams model output over server-sent events into a chat interface that stays smooth across thousands of messages. We have written our position on the cryptography underneath down rather than just asserting it — see [Encryption Explained](https://nerdy.pro/blog/encryption-explained), [How End-to-End Encryption Works](https://nerdy.pro/blog/how-end-to-end-encryption-works), and [SSL Certificate Pinning in Flutter](https://nerdy.pro/blog/ssl-certificate-pinning-flutter). ### What Healthcare Apps Actually Demand Six concerns come up in every healthcare build. Here is how we handle each. #### Sessions that survive a real network A patient takes a consultation on mobile data, in a corridor, switching from Wi-Fi to LTE halfway through. Video runs over WebRTC through a provider SDK; chat, presence, and session state run over a WebSocket. Both will drop, and the design question is not how to prevent that but what happens next. The failure modes are specific, and they are always the same three: a socket that reconnects but never re-subscribes, so the app looks connected and receives nothing; messages sent while offline that are lost rather than queued; and a client that shows a stale "in session" state after the server has already torn the session down. We treat delivery state as first-class UI — sent, delivered, read, failed — because in a clinical conversation "did they get that?" is not a cosmetic question, and a silent failure is worse than a visible one. #### Patient data, and the rules that govern it We do not hold HIPAA attestation or SOC 2 certification, and any agency that offers one as a selling point rather than describing how they build is selling you something. HIPAA obligations attach to the covered entity and its business associates — where HIPAA applies at all, that usually means you, your hosting provider, and your video vendor, under signed Business Associate Agreements. Our job is to build so that your compliance work is achievable rather than a rewrite: - Credentials and tokens in the iOS Keychain and Android Keystore, never in shared preferences - TLS with certificate pinning, and a rotation plan that does not brick older clients - Biometric or passcode gating on access to protected health information, not just at app launch - Encrypted local storage for anything cached on device, with a defined retention and wipe-on-logout policy - No health data in logs, crash reports, or analytics payloads — the most common real-world leak we find - As little data on the device as the product can get away with, because data that was never cached cannot be extracted from a stolen phone GDPR adds its own architectural constraints, and health data is a special category under Article 9, so processing needs a specific condition to sit on — and where you rely on consent, it has to be explicit and revocable. An erasure request has to be a query rather than an archaeology project, and where the data physically sits becomes a design decision rather than a hosting detail. These are choices you make early or pay for later. #### Appointments are instants, not strings This is healthcare's equivalent of the money-arithmetic problem in [fintech](https://nerdy.pro/services/flutter-app-development/fintech). An appointment is an instant in time that two people in two time zones have to agree on. Store it as a local wall-clock string and you will eventually move a consultation by an hour when a daylight-saving boundary lands between booking and session, or show a clinician in Berlin a slot that a patient in Lisbon booked for a different hour entirely. We store instants in UTC, keep the originating time zone alongside it where the display rule needs it, render in each participant's local zone, and test the boundaries deliberately — a booking made before a DST change for a session after it, a patient who travels between the two, a reminder that has to fire at the right local time on a device whose zone changed since the notification was scheduled. Reschedules and cancellations are the same problem with a state machine attached, and no-show handling is where the edge cases collect. #### Health data on the device Readings from Apple HealthKit, Android Health Connect, and Bluetooth Low Energy peripherals — cuffs, scales, glucometers, wearables — reach a Flutter app through platform channels. Some vendor SDKs ship Flutter packages; native-only ones need a wrapper, which is a known quantity of work rather than a risk, but it needs to be in the estimate. The part teams underestimate is permissions and gaps. Health permissions are granular, revocable, and different on each platform; background delivery has its own rules; and a sync that assumes continuous data will render nonsense the first time a user leaves their watch on the charger for two days. Deciding what your app does with missing data is a product decision, and it is better made before the chart is built. #### Accessibility, which is not a nice-to-have here Healthcare apps are used by people with low vision, tremor, hearing loss, cognitive load from illness, and by carers operating on someone else's behalf. That makes accessibility a functional requirement rather than a compliance checkbox: dynamic type that does not shatter the layout at 200%, contrast that survives a bright waiting room, touch targets sized for an unsteady hand, screen-reader labels on every control that carries meaning, and captions on any video content. It is also increasingly a legal question. The European Accessibility Act has applied to a range of consumer digital services in the EU since June 2025, and US federal procurement carries Section 508 requirements — whether a specific product falls in scope is a question for your counsel, but the engineering answer is the same either way. Flutter feeds both platforms' accessibility APIs from the semantics tree its `Semantics` widget builds, so this is achievable; it is simply work that has to be specified rather than discovered at the end. #### Store review, which is stricter here than most teams expect Apple reviews medical apps under guideline 1.4.1 and can ask for the methodology and regulatory clearance behind any clinical claim. Google Play's Health apps policy carries its own declarations, and telehealth and prescription features have country-by-country requirements. None of this is a reason not to ship, but it is a reason to settle three things before the feature is built: what the app claims to do, who backs that claim, and which markets it launches in. Teams that discover this at submission lose weeks. ### Why Flutter for Healthcare The honest case for Flutter here: | Requirement | How Flutter handles it | | --- | --- | | iOS and Android from one team | One codebase, roughly 30–40% less than two native builds | | Consistent clinical UI across devices | Flutter draws its own pixels, so a booking or consultation flow is identical on both platforms | | Reliable video and chat sessions | WebRTC and WebSocket SDKs are available; the reliability work is in the reconnection design, not the framework | | Platform security primitives | Keychain, Keystore, and biometrics reached through platform channels | | HealthKit and Health Connect | Reached through platform channels; well-established community packages cover the common cases | | Accessibility | Both platforms' accessibility APIs are fed from the `Semantics` tree, with dynamic type and contrast handled in the design system | The saving is not a discount on engineering — it is the removal of duplicated work: one booking flow, one consent flow, one set of clinical screens, tested twice rather than built twice. Where native still wins: if your product is essentially a wrapper around a platform-specific capability — a deep Apple Watch experience, or a device integration that exists only as an iOS framework — the cross-platform advantage shrinks, and native may be the honest answer. We will tell you when that is the case. The general service page is [Flutter app development](https://nerdy.pro/services/flutter-app-development). ### How We Work **Fixed-scope build.** We own delivery end to end — discovery, architecture, build, store submission. Best when you want a defined scope handed off and a team accountable for the outcome. Our [development process](https://nerdy.pro/blog/flutter-app-development-process) describes what that looks like week to week. **Staff augmentation.** Our engineers join your existing team, in your repo and your sprints, under your management. Best when you already have engineering leadership and need Flutter capacity — see [team augmentation](https://nerdy.pro/services/team-augmentation) and our [guide to hiring Flutter developers](https://nerdy.pro/blog/hire-flutter-developers-2026), which is honest about when not to use us. **Rescue and stabilisation.** Sometimes the app already exists and is failing in production, which is how the YouMi engagement started. That work is scoped against the symptoms, not against a feature list — it now has [its own page](https://nerdy.pro/services/flutter-app-development/app-rescue). Whichever route we take, we send a scoped quote within two business days of understanding the requirements. ### What a Healthcare App Costs A focused first release — accounts, booking, and one consultation channel — is a Business-tier build: $30–60K, over three to five months. A full telehealth platform with video consultations, separate clinician and patient apps, integrations into an existing clinical system, and health-device data is enterprise work, running from $90K+. These are the same published tiers as on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page — a vertical does not get a second price list. What moves a healthcare number within them is specific: the video vendor, the depth of integration into clinical systems you do not control, and the compliance decisions that determine where data may live. All three surface during scoping, which is why the quote comes after the questions rather than before them. ### Frequently Asked Questions Common questions from teams building healthcare and telemedicine products on Flutter. #### Is Flutter good for healthcare apps? Yes. Flutter is well suited to healthcare apps because it renders its own UI, which means booking flows, consultation screens, and symptom trackers behave identically on iOS and Android from a single codebase, typically at 30 to 40 percent less than building two native apps. It compiles to native code, so a consultation screen stays smooth while video and chat run on top of it, and both platforms' accessibility APIs are fed from the tree its Semantics widget builds, which matters more in healthcare than in most categories. The trade-off is that platform capabilities such as HealthKit, Health Connect, Bluetooth peripherals, and some video SDKs are reached through platform channels rather than directly, which is routine but real engineering work. #### Can a Flutter app be HIPAA compliant? Yes, in the sense that any app can be. HIPAA compliance is a property of your organisation and your data handling, not of your UI framework, and the obligations attach to the covered entity and its business associates under signed Business Associate Agreements — where HIPAA applies at all, that is typically you, your hosting provider, and your video vendor. Flutter uses the same platform primitives a native app uses for the technical safeguards: the iOS Keychain, the Android Keystore, biometric APIs, and TLS with certificate pinning. What determines whether a health app is safe is implementation discipline. The failures we find in practice are protected health information written to logs and crash reports, credentials in insecure storage, and unnecessary caching on the device, and all three appear just as often in native codebases. #### How do you build video consultations in a Flutter app? Through a WebRTC provider SDK rather than a WebRTC stack of your own — the media plumbing, TURN infrastructure, and codec negotiation are not where a healthcare product should spend its engineering budget. The work that actually determines quality is around the call: a waiting room that tells both sides what is happening, permission handling for camera and microphone that recovers when a user declines and changes their mind, and reconnection behaviour when the network changes mid-session. A consultation that drops at minute eleven is a clinical appointment that did not happen, so reconnection is designed in from the start rather than added after a complaint. #### Can Flutter apps read Apple HealthKit and Android Health Connect data? Yes, through platform channels, and well-established community packages already cover the common cases. The engineering effort is rarely in the read itself. It is in permissions, which are granular, revocable, and shaped differently on each platform, in background delivery rules, and in what your app does when the data has gaps — a user who left their watch on the charger for two days will otherwise be shown a chart that implies something clinically untrue. Deciding how missing data is displayed is a product decision worth making before the chart is built. #### How much does it cost to build a telemedicine app? A focused first release with accounts, booking, and one consultation channel typically runs three to five months of build. A full telehealth platform with video, clinician and patient apps, integrations into an existing clinical system, and health-device data runs longer, because the timeline is driven by third-party integrations, compliance decisions, and store review more than by UI work. Our breakdown of how scope, backend, integrations, and compliance compound into a final number is in our guide to Flutter app development cost. #### Do you have HIPAA or SOC 2 certification? No, and you should be sceptical of any app agency that offers a certification as a selling point rather than describing how they build. HIPAA has no certification scheme at all, only attestation and audit against the rule, and both apply to the organisation handling the data rather than to a contractor writing an app. What we bring is architecture that keeps protected health information out of logs, analytics, and unnecessary local storage, keeps credentials in hardware-backed platform keystores, and makes a GDPR erasure request a query rather than an excavation — so your compliance work is achievable instead of a rewrite. #### Why does accessibility matter more in healthcare apps? Because of who uses them. A healthcare app's users include people with low vision, tremor, hearing loss, and the cognitive load that comes with being unwell, plus carers acting on someone else's behalf. Dynamic type that survives a 200 percent setting, contrast that works in a bright waiting room, touch targets sized for an unsteady hand, and screen-reader labels on every meaningful control are functional requirements, not a compliance checkbox. There is a legal dimension too — the European Accessibility Act has applied to a range of consumer digital services in the EU since June 2025, and US federal procurement carries Section 508 requirements — but the engineering answer is the same whether or not your product is in scope. Read the cost breakdown in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026), or see the [YouMi case study](https://nerdy.pro/portfolio/youmi) for what a shipped Flutter telehealth product looks like. ## Marketplace App Development https://nerdy.pro/services/flutter-app-development/marketplace Marketplace app development is the engineering of two-sided products — apps where one group offers something and another group finds it: services, rentals, gigs, second-hand goods, on-demand help. It differs from ordinary app development in one structural way: you are shipping two products that must launch as one. Every feature exists twice from different angles — a listing is created by one side and browsed by the other, a booking is requested by one and accepted by the other — and the product only works when both experiences do. Flutter fits this shape well for a blunt economic reason: a marketplace already carries double product surface, and paying double platform cost on top of it is how two-sided budgets die. One codebase covers buyer and seller flows on both iOS and Android — and where the two roles diverge enough to become separate apps, both still come from the same code, the same way our [white-label fleet](https://nerdy.pro/services/whitelabel-app-development) does it. #### The products we build - **Local services marketplaces** — providers and seekers in the same area, as in [OneTwoDo](https://nerdy.pro/portfolio/onetwodo), which we designed and shipped end to end - **On-demand products** — request, match, fulfill: cleaning, repairs, delivery-style workflows - **Rental and booking marketplaces** — inventory with calendars, availability, and the double-booking problems they bring - **Peer-to-peer goods** — listings, offers, and the chat where the actual deal happens - **B2B and niche marketplaces** — where supply is curated and onboarding one side is an operations problem the app must support ### Who Builds Your Marketplace App #### A live marketplace, designed and shipped in seven weeks [OneTwoDo](https://nerdy.pro/portfolio/onetwodo) is a two-sided local services marketplace for Perform Connect Studios S.L. — cleaning, repairs, plumbing and more, posted by providers and browsed by neighbors. We built the whole product: the design, the Flutter client, and everything it runs on. Multi-currency pricing, per-listing localization, and a feed filtered by language and location shipped inside those seven weeks, in 2024. The build is also a worked example of marketplace scoping done honestly. A two-sided product normally implies a backend team before anything else; OneTwoDo shipped without writing that layer — Firebase for accounts, listings, photos, and analytics — which is what let one team carry design, client, and backend at once. Whether that architecture fits *your* marketplace is a scoping question we answer before the estimate, because it moves both the number and the calendar. The [MVP page](https://nerdy.pro/services/flutter-app-development/mvp) tells the same story from the validation angle. #### The adjacent muscles: feeds, chat, real-time A marketplace is usually a feed plus a conversation plus a transaction. We have shipped each at production depth: [Jepta](https://nerdy.pro/portfolio/jepta)'s location-aware feed and real-time chats and channels, [Arcana](https://nerdy.pro/portfolio/arcana)'s streaming chat holding thousands of messages at 60 fps, and the payments discipline described on our [fintech page](https://nerdy.pro/services/flutter-app-development/fintech) — led by a founder who ran card processing at scale. ### What Marketplace Apps Actually Demand #### The cold-start problem is a product decision the app must serve Every marketplace opens empty, and the app either helps or makes it worse. Helping looks concrete: a browsing experience that is useful before critical mass (curated categories rather than a barren search box), supply-side onboarding so low-friction a provider lists in minutes from a phone, and geographic focus built into the data model — winning one neighborhood beats being empty everywhere, which is why OneTwoDo's feed filters by location and language at the query level rather than as an afterthought. #### Two roles, one codebase — decided deliberately One app with a role switch, or two apps from shared code? The answer is per-product: a role switch when most users may eventually do both (peer-to-peer goods), separate apps when the provider side is a work tool with its own workflows (on-demand fleets). We have shipped the machinery for both — the cashback platform's consumer fleet and its separate merchant app come from one codebase. What stays constant is that both sides are specified together, because every marketplace feature is one flow crossing two screens owned by different people. #### Search and matching are the product Buyers judge a marketplace by whether the first screen shows something relevant. That is server-side work — indexed search, filters that mirror how supply is actually described, ranking that balances freshness, proximity, and quality — surfaced through a client that keeps scroll position stable under pagination and renders image-heavy cards without stutter on mid-range hardware. The catalog mechanics overlap our [e-commerce page](https://nerdy.pro/services/flutter-app-development/e-commerce); the difference is that marketplace inventory is messy, user-generated, and always partly stale, and the UX has to absorb that. #### Trust machinery: reviews, profiles, chat Strangers transact when the app gives them reasons to. Verified profiles, reviews that resist retaliation and spam, and in-app chat where the deal actually happens — with photos, offers, and enough structure that support can later reconstruct what went wrong. Chat is real-time engineering (typing, delivery, reconnection over a WebSocket), and moving conversations off-platform is a leak the design has to price in, not a moderation rule that fixes itself. #### Payments, payouts, and the money in between Marketplace money is harder than shop money: a payment comes in, a commission is held back, a payout goes out, and a dispute can happen at any point in between. Provider choice matters more than UI here — marketplace-grade rails like Stripe Connect exist precisely so escrow-shaped flows and seller onboarding (with its KYC obligations) are the provider's compliance problem rather than yours. Amounts are integers in minor units end to end, per [the rule that never bends](https://nerdy.pro/blog/floating-point-and-money). And both stores take no cut of physical goods and real-world services — but the moment you sell digital items, in-app purchase rules apply, and the line is worth drawing during scoping rather than during review. #### Store review with user-generated content Listings, photos, chat, and reviews make a marketplace UGC by definition, and Apple's guideline 1.2 requires the full apparatus: content moderation, a report mechanism, user blocking, and published terms. Teams discover this in the final week and ship late; we budget it as scope, together with account deletion and the data-safety declarations both stores demand. ### Why Flutter for a Marketplace | Requirement | How Flutter handles it | | --- | --- | | Two roles × two platforms | One codebase where native would mean four builds' worth of surface | | Both stores at launch | Supply and demand rarely share a platform; missing one store halves a two-sided funnel | | Image-heavy feeds on cheap phones | Native-compiled rendering keeps listing grids smooth on mid-range Android | | Chat that feels instant | A real-time layer built once serves both roles — proven on Jepta and Arcana | | A v1 that can iterate weekly | Sub-second hot reload while you learn what your liquidity actually needs | Where native still wins: if the product is essentially one platform's capability — an App Clip-first flow, deep platform-specific hardware — the cross-platform saving is not the point, and we will say so. The general service page is [Flutter app development](https://nerdy.pro/services/flutter-app-development). ### How We Work **Fixed-scope build.** We own delivery end to end — discovery, design, architecture, build, store submission. OneTwoDo is this model. Our [development process](https://nerdy.pro/blog/flutter-app-development-process) describes it week to week. **Staff augmentation.** Our engineers join your existing team, in your repo and your sprints — see [team augmentation](https://nerdy.pro/services/team-augmentation). Either way, we send a scoped quote within two business days of understanding the requirements. ### What a Marketplace App Costs A first release that proves liquidity — listings, search, profiles, chat or booking, one payment flow — is typically an MVP-to-Business-tier build: from $15–30K for a lean managed-backend validation build like OneTwoDo, to $30–60K over three to five months when the product needs its own backend, reviews, and payout flows. Separate provider apps, marketplace-grade payment rails with escrow, or operations tooling push into enterprise territory: from $90K+. These are the same published tiers as on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page — a vertical does not get a second price list. What moves a marketplace number within them: whether the two roles are one app or two, how much matching intelligence v1 really needs, and where between "link to Stripe checkout" and "held funds with payouts" your money flow sits. ### Frequently Asked Questions Common questions from founders building two-sided products on Flutter. #### Is Flutter good for marketplace apps? Yes, and the argument is structural: a marketplace already doubles your product surface because every feature exists for both a supply side and a demand side, and Flutter stops you paying double platform cost on top of that. One codebase covers buyer and seller flows on iOS and Android, image-heavy listing feeds stay smooth on mid-range devices because Flutter compiles to native code, and the real-time chat layer a marketplace needs is built once. We designed and shipped OneTwoDo, a live two-sided services marketplace, in seven weeks on exactly this stack. #### How much does it cost to build a marketplace app? A lean first release on a managed backend starts at our published MVP tier, a marketplace with its own backend, reviews, and payout flows is a Business-tier build over three to five months, and separate provider apps or escrow-style payment rails price as enterprise work — the ranges are published on this page and the Flutter app development page rather than quoted privately. The three questions that move the number most: one app or two, how smart matching needs to be at launch, and how deep the money flow goes. #### Should the buyer and seller be one app or two? It depends on whether the seller side is an identity or a workplace. A role switch inside one app fits products where many users eventually do both, like peer-to-peer goods. Separate apps fit products where providers work in the app all day and need workflows buyers never see, like on-demand fleets. Both apps still come from one Flutter codebase — we run a fleet of fifteen consumer apps plus a separate merchant app from a single codebase, so the second app is an output of the build, not a second project. #### How do you solve the empty-marketplace problem? The cold-start problem is won by operations and focus, but the app decides whether those efforts compound. Concretely: provider onboarding so light that supply lists from a phone in minutes, a browse experience that is useful before critical mass through curated categories rather than an empty search box, and geographic focus built into the data model so the product can win one area at a time. OneTwoDo's feed filters by location and language at the query level for exactly that reason. #### How do marketplace payments and payouts work? Through marketplace-grade payment rails rather than a plain checkout: money comes in from the buyer, a commission is held back, a payout goes to the seller, and a dispute can interrupt any step. Providers like Stripe Connect exist to make seller onboarding, held funds, and the associated KYC obligations the provider's compliance problem instead of yours, and choosing that rail early is worth more than any amount of checkout UI. In the app, amounts are integers in minor units end to end, and payment state is owned by the server, never assumed by the client. #### What does App Store review require from a marketplace? Treat a marketplace as user-generated content, because listings, photos, chat, and reviews make it one. Apple's guideline 1.2 expects a moderation method, a report mechanism, the ability to block users, and published terms — an app without them can be rejected regardless of how polished the rest is. Add the universal requirements, account deletion and data-safety declarations, and it becomes a real work package. We budget it as scope from the start, which is one of the reasons our submissions are a phase rather than a gamble. Read the [OneTwoDo case study](https://nerdy.pro/portfolio/onetwodo) for the full seven-week build, the [timeline breakdown](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) for where the calendar goes, or start from the [Flutter app development](https://nerdy.pro/services/flutter-app-development) pillar. ## MVP Development https://nerdy.pro/services/flutter-app-development/mvp MVP development is building the smallest app that can answer the question your business actually has — will anyone use this, will they pay for it, is the workflow you imagined the one people actually want. It differs from ordinary app development in one structural way: the deliverable is not a feature set — it is an answer. Everything that does not move you closer to that answer is scope you are paying for twice, once to build and once to maintain until you delete it. An MVP on Flutter typically takes **two to three months** from kickoff to a live store listing, and runs $15–30K. Both figures are the ones in our [cost guide](https://nerdy.pro/blog/flutter-app-development-cost-2026), and where those months go, phase by phase, is in our [timeline breakdown](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) — we would rather publish them and be argued with than open with "it depends". Flutter matters more at this stage than at any other. An MVP tests demand, not platforms, and you do not know in advance which store your first hundred real users will come from. One codebase puts the app on both, which is the difference between validating an idea and validating an idea *on Android*. #### What an MVP ships with - **5–8 core features** — the ones the question depends on, and no others - **iOS and Android**, from one codebase and one team - **A hosted backend** rather than one written from scratch, unless something in the product genuinely requires otherwise - **Component-library UI** — Material and Cupertino, styled rather than redrawn, so design is not the critical path - **Analytics and crash reporting** from day one, because an MVP that ships without instrumentation answers nothing - **A listing on both stores**, with the submission work treated as a phase rather than a button ### Who Builds Your MVP Two things separate a team that can ship an MVP from one that has only built apps. #### A full product, designed and shipped, in seven weeks [OneTwoDo](https://nerdy.pro/portfolio/onetwodo) is a two-sided local services marketplace for Perform Connect Studios S.L. — listings for cleaning, repairs, plumbing, and the rest, posted and browsed by people in the same area. We built it end to end: product design, the Flutter client, and everything it runs on. Seven weeks, in 2024. It fits here because of *how* it was scoped. A two-sided marketplace normally implies a backend team before it implies anything else — accounts, sessions, a listings database, photo storage, a server to put it behind — and that is usually where a small team's first three months go. OneTwoDo shipped without writing that layer: Firebase Authentication for accounts, Firestore for listings and profiles, Cloud Storage for photos, Analytics and Crashlytics for the operational visibility a team would otherwise build dashboards for. None of it needed provisioning, patching, or an on-call rotation. That decision is what let one team carry design, client, and backend at once, and it kept paying out: multi-currency pricing, per-listing localisation, and a feed filtered by language and location were each client-side work against a data layer that already existed, rather than a backend ticket followed by a client ticket. The full write-up is in the [case study](https://nerdy.pro/portfolio/onetwodo). #### We publish the numbers instead of "it depends" Most agencies answer "how long" and "how much" with a range wide enough to be useless. Ours are on this page, on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page, and broken down phase by phase in [How Long Does It Take to Build a Flutter App?](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) and [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026). If your project does not fit them we will say so during scoping, which is the only point at which that information is worth anything to you. Another build at this size: [Arcana](https://nerdy.pro/portfolio/arcana), an AI tarot companion whose Flutter client — a streaming chat holding thousands of messages at 60 fps — took three weeks. We have also written up the [six-week plan for turning a Telegram bot with an existing user base into a store app](https://nerdy.pro/blog/telegram-bot-to-app-6-weeks); that one is a composite of work we have done rather than a single client, and it is fast precisely because the product question had already been answered elsewhere. ### What an MVP Actually Demands Six things decide whether an MVP ships in three months or becomes a project three times the length it was scoped as. #### A cut list you actually honour The two-month end of the range is real, but conditional: a scope that fits on one page, one person who can approve a decision without convening a committee, and no novel technical risk — no video pipeline, no on-device ML, no payment rails in a new jurisdiction. Three months is the more common outcome, and the extra month is almost never "the code was harder". It is a payment flow that appeared in week four, an unbudgeted design round, or two weeks waiting on API access. So the cut list is the deliverable of scoping, not a footnote to it. We write down what is out as explicitly as what is in, and we will push back on additions mid-build — not to be difficult, but because every small addition is a week, and four small additions are the difference between launching this quarter and next. #### The scope you cannot see "A simple app to track workouts" is five screens in your head. In practice it is onboarding, authentication with email and Google and Apple — each with its own edge cases — the actual feature, settings, profile, account deletion (required by both stores), and a payment flow if it is monetised. None of that is padding, and none of it is what you were thinking about when you described the app. Naming this early is most of what accurate scoping is. It is also where the honest conversation about the range happens: the invisible scope is roughly constant, so it consumes a much larger share of a two-month build than of a six-month one. #### A backend you did not write from scratch For most MVPs, a managed backend is not a compromise — it is the correct engineering decision, and OneTwoDo is the case in point. Firebase, Supabase, or a thin REST service over a managed database covers accounts, data, files, and analytics without a team to run it, and it will carry you well past the point where an MVP has answered its question. Where we would advise against it: when the product's core is something a managed backend cannot do — heavy custom business logic, real-time Fan-out at scale, a compliance regime that dictates where data physically sits. We will tell you which side you are on before the estimate, because it moves the number. #### Instrumentation, or the MVP answers nothing An MVP exists to produce evidence. Shipping one without analytics, crash reporting, and a defined set of events is spending the whole budget and getting an opinion back instead of data. During scoping we agree the handful of events that correspond to your actual question — activation, the core action, the drop-off you are worried about — and wire them before launch rather than after the first cohort has already been and gone. #### The parts we refuse to cut An MVP earns its speed by cutting features, not by cutting engineering. What stays regardless of scope: module boundaries that let v2 grow without a rewrite, CI/CD from the first week so a build is one command, tests on the paths where being wrong costs money, and named error states instead of a spinner that never resolves. This is the difference between an MVP and a prototype, and it is worth being precise about. A prototype is disposable by design. An MVP is the first version of a product you intend to keep, so the code has to be code you can keep — and the reason "we'll clean it up after launch" so rarely happens is that a validated product immediately generates more urgent work than cleanup. #### Store submission is a phase Budget one to two weeks. Apple's review is usually 24–48 hours and Google's is usually faster, but the time that actually goes missing is the rejection cycle — a 24-to-72-hour round trip, and a first submission that gets one is normal. The work around the review is not free either: store listings, screenshots for every required device size, a privacy policy and the data-safety declarations both stores now demand, and an account-deletion path Apple will check for. Teams that discover this in the final week ship late for reasons that have nothing to do with their app. ### Why Flutter for an MVP The honest case for Flutter here: | Requirement | How Flutter handles it | | --- | --- | | Both stores, one budget | One codebase, roughly 30–40% less than two native builds — the largest cost lever once the scope is fixed | | Iterate while users are watching | Sub-second hot reload; a change is on a device in the time a native rebuild takes to start | | Design not on the critical path | Material and Cupertino widgets carry the UI until you have earned a custom design system | | One team to hire | Flutter engineers, not an iOS team plus an Android team plus the coordination between them | | v2 without a rewrite | The MVP codebase is the product codebase — the same app grows into a full business app rather than being replaced | Where native still wins: if the question you are validating *is* a platform-specific capability — a deep Apple Watch experience, a widget-led product, something built on an API that exists on one platform only — then the cross-platform saving is not the point and native may be the honest answer. We will tell you when that is the case. The general service page is [Flutter app development](https://nerdy.pro/services/flutter-app-development). ### How We Work **Fixed-scope MVP build.** We own delivery end to end — discovery, design, architecture, build, store submission. Best when you want a defined scope handed off and one team accountable for the launch date. Our [development process](https://nerdy.pro/blog/flutter-app-development-process) describes what that looks like week to week. **Prototype to production.** If you already have something running — a Cursor, Bolt, or Lovable build, or a long weekend with an AI agent — the product question may be partly answered already, and the work is different: an audit, then the gap between "works in the demo" and "safe in front of real users". That path is [AI code audit](https://nerdy.pro/services/ai-code-audit), and the playbook is in [Shipping an AI-Built Prototype to Production](https://nerdy.pro/blog/ai-prototype-to-production). **Staff augmentation.** Our engineers join your existing team, in your repo and your sprints, under your management. Best when you already have engineering leadership and need Flutter capacity — see [team augmentation](https://nerdy.pro/services/team-augmentation) and our [guide to hiring Flutter developers](https://nerdy.pro/blog/hire-flutter-developers-2026), which is honest about when not to use us. Whichever route we take, we send a scoped quote within two business days of understanding the requirements. ### Frequently Asked Questions Common questions from founders scoping a first release on Flutter. #### What is an MVP in app development? A minimum viable product is the smallest app that can answer a specific business question — will people use this, will they pay, is this the workflow they want. The emphasis belongs on viable rather than minimum: it has to be good enough that a real user's reaction tells you something true, which is why an MVP is not a prototype or a clickable mockup. In practice that means 5 to 8 core features, a real backend, analytics wired to the question you are asking, and a live store listing. Everything beyond that is scope you pay for twice, once to build and once to maintain until you delete it. #### How long does it take to build an MVP? Two to three months from kickoff to a live store listing, for a Flutter MVP on both iOS and Android. The two-month end is real but conditional: a scope that fits on one page, one person who can approve decisions without a committee, and no novel technical risk such as a video pipeline, on-device machine learning, or payment rails in a new jurisdiction. Three months is more common, and the extra month is rarely the engineering. It is a payment flow that appeared in week four, an unbudgeted design round, or two weeks waiting on access to someone else's API. #### How much does it cost to build an MVP? Our MVP tier is published on this page and on the Flutter app development page rather than quoted privately, and it covers 5 to 8 features on iOS and Android with a hosted backend and a component-library UI. What moves the number is scope first and integrations second — each significant third-party integration is one to two weeks, and the invisible scope of onboarding, authentication, settings, account deletion and payments is roughly constant, so it takes a larger share of a small build than a large one. The full breakdown of what drives the figure is in our guide to Flutter app development cost. #### What should be in an MVP, and what should be cut? In: the features the question depends on, one clean path through them, analytics on the moments that answer it, and the plumbing the stores will not let you ship without, which includes an account-deletion path. Out: the second user role, the admin panel you could run from a spreadsheet for six months, the custom design system, offline support, and every feature justified by a user you have not met yet. The useful test is whether removing something changes what you learn from the launch. If it does not, it is v2 — and writing that cut list down is the deliverable of scoping, not a footnote to it. #### Will we have to rewrite the MVP later? Not if it was built as an MVP rather than a prototype. A prototype is disposable by design; an MVP is the first version of a product you intend to keep, so it should be built with module boundaries that let features grow, continuous integration from the first week, tests on the paths where being wrong costs money, and real error states. Those cost days, not weeks, and they are the difference between v2 being an extension and v2 being a restart. What we do cut for speed is features, bespoke design, and infrastructure you can rent — never the structure of the code itself. #### Can you build an MVP from an AI-generated prototype? Yes, and it is increasingly where projects start. A Cursor, Bolt, or Lovable build often means the product question is partly answered already, which is genuinely valuable — but the gap between working in a demo and being safe in front of real users is specific and predictable, clustering into exposed keys and missing authorisation, uncached API calls that overspend by multiples, and sensitive data reaching logs or third parties. The route in that case is an audit first, then the shipping work, rather than a rebuild that throws away what you have already learned. #### Should an MVP launch on both iOS and Android? Usually yes, and this is the strongest argument for Flutter at this stage specifically. An MVP tests demand, not platforms, and you rarely know in advance which store your first real users arrive from — so shipping to one is validating your idea on one platform's audience and then guessing about the other. Because a single Flutter codebase produces both apps, the marginal cost of the second store is small, which is exactly the trade a first release wants. The exception is a product whose core is a platform-specific capability, where the cross-platform saving is not the point. Read the phase-by-phase timeline in [How Long Does It Take to Build a Flutter App?](https://nerdy.pro/blog/how-long-to-build-a-flutter-app), or see the [OneTwoDo case study](https://nerdy.pro/portfolio/onetwodo) for a full product designed and shipped in seven weeks. ## Nuxt Development https://nerdy.pro/services/web-development/nuxt-development Nuxt development is building web applications on Nuxt, the full-stack framework that wraps Vue in what a production site needs: server-side rendering, file-based routing, server API routes, image optimisation, and a content layer for Markdown-driven pages. A Nuxt site ships real HTML on the first response, so crawlers, link previews, and slow phones get the page instead of a spinner that promises one. We are not neutral about this framework, and we do not pretend to be. This site — four locale domains, Markdown content collections, generated sitemaps, structured data on every page — is Nuxt, and we feel the consequences of every architectural choice in our own search traffic before we recommend it to anyone. #### The products we build - **Marketing and content sites** — fast, indexable, and editable in Markdown or a CMS, priced as their own tier on the [web development](https://nerdy.pro/services/web-development) page - **Content platforms** — blogs, documentation, multi-language editorial sites where structured data and hreflang do real work - **Product web apps** — authenticated, interactive applications where the public pages still have to rank; the back-office variant is covered on [admin panels & dashboards](https://nerdy.pro/services/web-development/admin-dashboards) - **Migrations onto Nuxt** — moving a frontend off server templates, WordPress, or a client-rendered SPA without stopping the product - **The web side of a mobile product** — where the app is the product and the web is its public face; that pairing has its own page, [companion web apps](https://nerdy.pro/services/web-development/companion-web-app) ### Who Builds Your Nuxt App Both proofs below are live, and you can check either one yourself. #### This site, which you are reading through Nuxt right now nerdy.pro runs on Nuxt 4 with SSR: multi-domain internationalisation across four locales, Nuxt Content driving every service and blog page, generated sitemaps, per-page JSON-LD, and Core Web Vitals we answer for with our own leads. Dogfooding is easy to claim, so we make it verifiable: view the source of this page and the full HTML is there, no JavaScript required. #### Formtastic — a frontend moved onto Nuxt under load [Formtastic's](https://nerdy.pro/portfolio/formtastic) interface began as Django templates: quick to start, increasingly expensive to evolve, every UI change coupled to a backend developer. We separated the system: Django stayed as the API, and the entire user interface moved to a Nuxt frontend with SSR and SSG. Screen development sped up, server load dropped, and the public pages gained measurably in SEO and loading speed. The web application is live at app.formtastic.de. The back office of our [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform) is Nuxt as well, where the framework's server routes and conventions pay for themselves even though SEO never enters the picture. ### What Nuxt Work Actually Demands The framework gives you rendering for free. Production Nuxt is everything around it. #### Rendering the right pages the right way SSR, SSG, and client-only rendering are per-route decisions, not a religion. A pricing page wants to be statically generated and cached at the edge; a dashboard behind a login wants no server rendering at all; a content page wants SSR with cache headers tuned to how often it changes. We set these choices explicitly per route, and we engineer out hydration mismatches, the classic Nuxt production bug, rather than patching around them. #### Content modelling before content entry The schema decides what a content site can do: what fields a page has, what is validated at build time, what an editor can and cannot break. We model content first, in Nuxt Content with type-safe schemas when developers own the words, or in a headless CMS when editors do. The trade-off between those two is laid out on the [web development](https://nerdy.pro/services/web-development) page. #### The SEO machinery most builds skip Meta and Open Graph tags rendered server-side, canonical URLs, hreflang across languages and domains, JSON-LD structured data, sitemaps that update themselves. This site emits all of it, page by page, from the same code we deploy for clients. None of it arrives by switching on a plugin. #### Performance as a budget Core Web Vitals are targets we build to: images sized and lazy-loaded through Nuxt's own pipeline, code split by route, caching set per content type. Server rendering does not make a page fast on its own; it only puts the speed under your control. ### Migrating to Nuxt Without Stopping the Product The Formtastic pattern generalises. The existing backend keeps serving as the API — Django, Rails, Laravel, PHP services, it does not matter — while the interface moves to Nuxt route by route, public pages first, where SSR pays immediately. No big-bang rewrite, no feature freeze, and each migrated route is measurable against its predecessor on speed and search before the next one moves. If your frontend is a client-rendered SPA that never ranks, the same approach applies; only the framework underneath is different. ### How We Work **Fixed-scope build.** Discovery, content model, build, deployment, including analytics wiring and the search-console handover, so you can measure the SEO results instead of taking our word for them. **Staff augmentation.** Our engineers join your team's repo and sprints — see [team augmentation](https://nerdy.pro/services/team-augmentation). Either way, we send a scoped quote within two business days of understanding the requirements. ### What Nuxt Development Costs Nuxt work is priced from the [web development](https://nerdy.pro/services/web-development) page's published tiers: a marketing or content site at $5,000 – $12,000, an authenticated web app at $15,000 – $40,000, and an editor-owned content platform on a headless CMS at $20,000 – $50,000. These are the same published tiers as the pillar's pricing table; the same keys render both, so the two cannot drift. A migration prices like the tier it lands in: moving a marketing site onto Nuxt is landing-tier work, moving an authenticated product frontend is web-app-tier work, with the existing API intact either way. ### Frequently Asked Questions Common questions from teams considering Nuxt for a build or a migration. #### What is Nuxt and why would we choose it? Nuxt is a full-stack web framework built on Vue that handles server-side rendering, routing, server API routes, and image optimisation out of the box. The practical reason to choose it: your pages arrive as real HTML, so search engines and link previews read them without executing JavaScript. That is the single biggest framework-level lever for organic search. We run our own site on it, so the recommendation comes from operating the framework, not from a comparison chart. #### Nuxt or Next.js? They are honest equivalents: Nuxt is to Vue what Next.js is to React, and both handle server rendering, routing, and full-stack conventions well. The deciding factor is almost never the framework: it is which ecosystem your team can hire for and maintain. Ours is Vue, we run our own business on Nuxt, and that operational depth is what we sell. If your team is deep in React and hiring against it, Next.js is the right call and we will say so. #### Can you migrate our existing site to Nuxt without a rewrite? Yes, and that is the Formtastic pattern. Your existing backend stays as the API while the interface moves to Nuxt route by route, public pages first where server-side rendering pays immediately. No feature freeze, no big-bang cutover, and each migrated route is measured against its predecessor on speed and search before the next moves. The same discipline applies whether you are leaving server templates, WordPress, or a client-rendered single-page app. #### Will moving to Nuxt actually improve our SEO? If your current frontend renders in the browser or couples every page to slow server templates, usually yes, and measurably. Server-side rendering puts content, meta tags, and structured data into the first HTML response, which is the most impactful framework-level decision for organic search. Formtastic's public pages gained in both SEO and loading speed after the move. What Nuxt cannot do is compensate for thin content. If your problem is the words rather than the rendering, we will tell you. #### How much does a Nuxt project cost? A fast marketing or content site typically runs 5,000 to 12,000 US dollars over two to four weeks, an authenticated web app 15,000 to 40,000 over one to three months, and an editor-owned content platform on a headless CMS 20,000 to 50,000. These are the published tiers on our web development page, not a starting position for a sales call. A migration prices like the tier it lands in, since the existing API stays. #### When would you advise against Nuxt? For a purely internal tool behind a login, where nothing needs indexing, a plain Vue single-page app is simpler to reason about and cheaper to run, and our own technology notes say exactly that. Also when your team is committed to React, where Next.js keeps you hiring from your own ecosystem. Nuxt earns its complexity when pages must be found, shared, or fast on first load; when none of those apply, we will recommend the simpler thing. Read the [Formtastic migration](https://nerdy.pro/portfolio/formtastic) case study, see why we reach for [Nuxt](https://nerdy.pro/technologies/nuxt) over plain [Vue](https://nerdy.pro/technologies/vue) when SEO matters, or start from the [web development](https://nerdy.pro/services/web-development) pillar. ## React Native to Flutter Migration https://nerdy.pro/services/flutter-app-development/flutter-migration ### What a Flutter Migration Actually Is If you're still weighing Flutter against React Native, read [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026) first — this page assumes you've already decided and answers the execution question instead: how, how long, how much, and how risky. A React Native to Flutter migration is a rewrite, not a port. Flutter and React Native share no build artifacts, no component library, and no state layer — every screen, navigation flow, and native integration is rebuilt from scratch in Dart. What survives the move is everything that isn't code: your business rules, your API contracts, your data models, and the design decisions your team already made. Those become the specification the new app is built against, which is why a migration is faster and lower-risk than starting from a blank page — but it is still, honestly, a full rebuild of the client. - **0** Shared Build Artifacts: React Native and Flutter compile to different runtimes — every screen is rebuilt in Dart - **Screen by Screen** Incremental Delivery: Flutter ships into your live app one screen at a time, never as a single cutover - **Zero Downtime** App Stays Live: Your existing app keeps shipping and serving users for the entire migration - **100%** Business Logic Carries Over: As a spec — your rules, API contracts, and data models transfer even though the code doesn't ### Who Should Migrate — and Who Shouldn't Flutter is usually the right call for a team already running React Native or native in production — but not always, and we'd rather tell you that up front than sell you a rewrite you don't need. #### Signs it's time to migrate - React Native's upgrade treadmill (New Architecture, breaking releases, dependency churn) is costing real engineering time every cycle - You're hitting a performance ceiling — jank on complex screens, slow list rendering, animation stutter — that the bridge architecture can't fix - Native modules have become a maintenance burden: forked packages, version-locked dependencies, code the original author left behind - Hiring is hard or inconsistent, and you want one team on one codebase instead of RN and native specialists - You're already building new work in Flutter and want the rest of the app to match #### When migration is NOT the right call - Your app is stable, performs well, and isn't blocking the roadmap - Your team is happy and productive, with no appetite for a multi-month rebuild - You've invested heavily in custom native modules that work fine and have no Flutter equivalent worth chasing - The case for migrating is "Flutter looks nice" rather than a real cost, performance, or maintenance problem > **The honest read:** if none of the "signs" above describe your app, don't migrate. A stable app with a happy team is the best app to leave alone. Tell us where you're actually stuck and we'll say plainly whether a migration is worth it — including "not yet." ### The Incremental Approach: Add-to-App The reason to migrate with us instead of commissioning a from-scratch rewrite is the delivery model, not the framework choice. We embed Flutter into your existing React Native or native app as a module — an add-to-app integration — and move screens across one at a time behind a shared router. Nothing about this requires a big-bang cutover, and nothing about it takes your app offline. And if what you actually want is Flutter *inside* a native app that stays native — adoption without the wholesale journey — that is its own engagement: see [add-to-app integration](https://nerdy.pro/services/flutter-app-development/add-to-app). #### The Add-to-App Flow 1. **Existing Shell Stays** Your current RN or native app keeps running in production, unchanged, as the host shell 2. **Flutter Module Embedded** A Flutter engine is added to the shell via add-to-app, sharing navigation and native services 3. **Screens Migrate One at a Time** Each screen ships in Flutter behind the router, tested and released independently 4. **RN Screens Retire** As each Flutter screen proves out in production, its RN equivalent is deleted, not archived 5. **Full Cutover** Once every screen has moved, the host shell itself is removed and the app is 100% Flutter This isn't friction-free. For the length of the migration you're running two toolchains and two state layers side by side — and, until the design system is fully ported, two visual languages in different corners of the same app. That's temporarily more complexity, not less. We scope the migration map up front specifically to keep that window as short as it can be, but we won't pretend it disappears. ### What Carries Over vs What's Rebuilt Not everything is thrown away. Here's what actually survives the move to Flutter, and what gets rebuilt from zero. | Layer | Carries Over | Rebuilt in Flutter | | --- | --- | --- | | Business logic & rules | Yes | — | | API contracts | Yes | — | | Data models | Yes | — | | Design tokens & brand system | Yes | — | | Test cases (as specs) | Yes | — | | UI screens | — | Rebuilt | | Navigation | — | Rebuilt | | State management | — | Rebuilt | | Native integrations | — | Rebuilt | | CI/CD pipeline | — | Rebuilt | ### How We Run a Migration #### 1. Audit We review your codebase — screens, navigation, state management, native modules, API surface, and test coverage — and flag what's straightforward to migrate versus what's going to be genuinely hard. Every landmine — a forked native module, an undocumented state machine, a backend contract nobody wrote down — gets found here, not three sprints into the rebuild. #### 2. Migration Map The audit becomes a screen-by-screen migration plan: sequencing, dependencies between screens, which native modules need Flutter equivalents, and a realistic order that keeps the app shippable at every step. This document is what the rest of the project is scoped against. #### 3. Incremental Delivery We embed Flutter into your existing shell and start shipping screens in priority order — usually starting with something low-risk to prove the pipeline, then moving into the screens that actually matter. Your app keeps releasing normally throughout; the migration runs alongside your regular roadmap, not instead of it. #### 4. Cutover Once every screen has moved and the old code has been deleted, we retire the host shell and ship a Flutter-only build. From here the app is maintained like any other Flutter codebase — one team, one toolchain. Prefer to keep the work in-house? Our [Team Augmentation](https://nerdy.pro/services/team-augmentation) engineers can embed with your team and run the migration under your ownership instead of ours. ### De-Risk Before You Commit You don't have to commit to a full migration to find out what one would involve. Our [AI Code Audit](https://nerdy.pro/services/ai-code-audit) service runs the audit phase above as a fixed-scope engagement on its own: a full review of your existing codebase and a written report that becomes your migration map — landmines, native-module risk, and a realistic sequencing — whether or not you move forward with us afterward. > **Start Here If You're Not Sure** > > An audit is the lowest-commitment way to find out whether a migration makes sense for your app, what it will actually cost, and where the real risk is — before anyone writes a line of Dart. ### Timeline & Cost A Flutter migration scales like a rebuild, because it is one — the codebase you end up with is a full Flutter app, built to the same scope as if you'd started from scratch. What the incremental approach changes is risk, not size: spreading delivery across screens stretches the calendar compared to a big-bang rewrite, but it means your app never goes dark, and you can stop, pause, or reprioritize at any screen boundary. We don't quote migration timelines or prices without seeing the app — the range is too wide to be useful otherwise. But since a migration is priced like a rebuild, the published tiers on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page are the honest anchor: an app at Business-tier scope — $30–60K to build fresh — is what a migration of a similar app tracks, plus the audit and migration-map phase up front. For how we estimate the underlying build, see [how long a Flutter app takes to build](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) and [what actually drives Flutter development cost](https://nerdy.pro/blog/flutter-app-development-cost-2026). ### Frequently Asked Questions Everything you need to know about migrating from React Native or native to Flutter #### Can you migrate a React Native app to Flutter incrementally? Yes — this is how we run every migration. We embed Flutter into your existing React Native app using an add-to-app integration and move screens across one at a time behind a shared router. Your app stays live and shippable throughout; there is no point where it goes offline for a rewrite. #### Is migrating from React Native to Flutter a rewrite? Yes, and we say so upfront. React Native and Flutter share no build artifacts, so the UI, navigation, state management, and native integrations are rebuilt from scratch in Dart. What carries over is your business logic, API contracts, and data models — as specifications the new code is built against, not as reused code. #### How long does a React Native to Flutter migration take? It depends on the size of the app and how many native modules and custom integrations it has — there is no fixed number that applies across apps. It tracks close to building an equivalent Flutter app from scratch, plus the audit and migration-map phase up front. The incremental approach stretches the calendar compared to a big-bang rewrite, in exchange for the app never going offline. #### How much does it cost to migrate to Flutter? A migration is priced like a rebuild of the client app, because that is what it is. We quote an exact figure only after auditing your specific app — the number depends heavily on native module count, backend integration complexity, and how much custom UI you have. #### Can you migrate a native iOS/Android app to Flutter? Yes. The same incremental, add-to-app approach applies to native Swift/Kotlin apps as it does to React Native — Flutter's add-to-app tooling embeds into an existing native host the same way. The audit and migration map look slightly different (no RN-specific risk like the New Architecture treadmill), but the delivery model is identical. #### What happens to our existing native modules? Each one gets audited individually. Some have a maintained Flutter or Dart FFI equivalent and swap over directly. Others need a thin platform-channel bridge written around your existing native code, which keeps it working without a rewrite. The migration map produced by the audit tells you which is which before any development starts. #### Should we migrate to Flutter at all? Not always. If your React Native app is stable, performs well, and your team is productive, migrating is a cost with no real problem behind it — we will tell you that directly rather than sell you a rewrite. Migration makes sense when you are fighting the upgrade treadmill, hitting a performance ceiling that cannot be cleared, carrying a native-module maintenance burden, or struggling to hire and keep a consistent team. If none of those describe your app, stay where you are. ## Social & Community App Development https://nerdy.pro/services/flutter-app-development/social Social and community app development is the engineering of products where the users make the content: chats and group messaging, channels and forums, feeds of posts and comments, neighborhood and interest communities. It differs from ordinary app development in two structural ways. The content is live — a message, a reaction, a new post has to appear now, not on next refresh — so a real-time layer sits under everything. And the content is other people's, which brings moderation, blocking, and store-review obligations that product plans routinely discover too late. Flutter carries this category well: chat lists, feeds, and media-heavy screens are exactly the custom, animation-dense UI it renders identically on iOS and Android from one codebase — and a community that launches on one platform has cut its network effect in half, which makes both stores on one budget a structural need rather than a saving. #### The products we build - **Community platforms** — chats, channels, posts, comments, and a feed, as in [Jepta](https://nerdy.pro/portfolio/jepta), shipped whole in six months - **Messaging-centred products** — where the conversation is the product, from group chat to AI companions like [Arcana](https://nerdy.pro/portfolio/arcana) - **Interest and neighborhood networks** — location- or topic-scoped communities with local feeds - **Community layers inside other products** — the chat, reviews, or forum attached to a marketplace, a health product, or a brand — see how it meets [marketplace](https://nerdy.pro/services/flutter-app-development/marketplace) and [healthcare](https://nerdy.pro/services/flutter-app-development/healthcare) work ### Who Builds Your Community App #### A full neighborhood platform, shipped whole [Jepta](https://nerdy.pro/portfolio/jepta) is a hyperlocal community app for Netgineers GmbH: neighborhood chats, business channels, posts, comments, and a feed that reorders itself around wherever the user is standing. It shipped to both stores from one Flutter codebase inside a six-month window with the full feature set in the first release — because the real-time layer, the navigation graph, and the state pattern were built once and reused, so every additional surface stayed a small change instead of a new integration. For a product whose roadmap is "more surfaces", that architecture is the part that compounds. #### Chat performance at the unreasonable end [Arcana](https://nerdy.pro/portfolio/arcana) is a chat holding thousands of streaming markdown messages at 60 fps — three weeks of build, and the hardest rendering case a conversation UI meets. [YouMi](https://nerdy.pro/portfolio/youmi) added the other discipline: chat inside a telehealth product, where a dropped conversation is not an annoyance but a failed appointment. Between the three, the chat stack — transport, state, rendering, reconnection — is muscle we have exercised repeatedly, not a feature we would be building for the first time on your product. > Six months for an app this wide was the part that worried me. Jepta is neighborhood chats, business channels, posts, comments and a feed that reorders itself around wherever you happen to be standing — a long list of screens, a fixed date, and the realistic expectation that half of it would slip into a second release. > > Nerdy Production built the shared foundations first — one real-time layer, one navigation model — and after that every new screen was cheap. We launched on iOS and Android with the full feature set, chat has been dependable from day one, and extending the app since then has been a small job rather than a project. We got the product we specified, on the date we specified. *[Evgeny Syrtsov](https://www.linkedin.com/in/evgeny-syrtsov/), CEO, Netgineers GmbH* ### What Community Apps Actually Demand #### One real-time layer, not one per feature Chat, presence, reactions, live comment counts — the naive build gives each its own plumbing and collapses under the maintenance. The durable build is one WebSocket layer with one reconnection policy, one message-ordering rule, and one place where "what happens when the socket drops mid-scroll" is answered — which is precisely how Jepta was structured, and why its post-launch screens stayed cheap. The transport decisions — how updates are coalesced, what the server may push and how often — matter more than anything in the widget tree, the same Fan-out discipline our [ExtraETF write-up](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go) describes for market data. #### A feed is an ordering policy wearing a UI Chronological, ranked, or — like Jepta — reordered by where the user physically is: the feed's policy is the product's personality, and it has to be executed at the query level so it stays correct under pagination, pull-to-refresh, and items arriving while the user scrolls. The client's job is to absorb live inserts without yanking the scroll position, keep media-heavy cells smooth on mid-range hardware, and degrade gracefully when the network does. #### Notifications that inform without burning out Push is a community product's heartbeat and its fastest path to an uninstall. The engineering half is unglamorous and essential: per-conversation and per-channel preferences, digesting ("12 new messages", not twelve bangs), deep links that land on the right message, and badge counts that agree with reality. The restraint half is product design, and we push back when a notification plan reads like an engagement extraction scheme — it costs retention, which is the metric a community actually lives on. #### Moderation is a launch requirement, not a growth feature The stores enforce what communities learn painfully: user-generated content requires a moderation method, a way to report content, a way to block users, and published terms — Apple rejects on guideline 1.2 without them. Beyond passing review, the machinery must exist operationally: reported content queued for a human, repeat offenders surfaced, and blocks that actually sever every surface where two users could meet. We scope this with the first release because retrofitting a block system across chats, comments, and feeds later is a rewrite of the social graph's edges. #### Identity, privacy, and the shape of the graph Who can see whom, find whom, message whom — decided before the data model exists, because every later answer is a migration. Neighborhood products add location privacy: Jepta's feed knows where you stand, and the design has to make that a feature rather than a leak — coarse locations by default, real ones only where the product genuinely needs them, and account deletion that actually unwinds a person from conversations they leave behind. ### Why Flutter for Community Products | Requirement | How Flutter handles it | | --- | --- | | Both stores at launch | A community split across platforms is half a community; one codebase makes both stores the default | | Chat-grade rendering | Native-compiled UI holds 60 fps through streaming, animated, media-heavy conversations — proven on Arcana | | Custom, brand-heavy UI | Flutter draws every pixel; a community's identity is not assembled from stock platform widgets | | A roadmap of "more surfaces" | One real-time layer and one navigation graph make each new screen a small change — the Jepta result | | Iteration while the community forms | Sub-second hot reload while you learn what your people actually do together | Where native still wins: if the product is essentially a platform capability — a widget-first experience, deep Watch presence, an iMessage-adjacent product — the cross-platform saving is not the point, and we will say so. The general service page is [Flutter app development](https://nerdy.pro/services/flutter-app-development). ### How We Work **Fixed-scope build.** We own delivery end to end — discovery, design, architecture, build, store submission. Jepta is this model at full width. Our [development process](https://nerdy.pro/blog/flutter-app-development-process) describes it week to week. **Staff augmentation.** Our engineers join your existing team, in your repo and your sprints — see [team augmentation](https://nerdy.pro/services/team-augmentation). Either way, we send a scoped quote within two business days of understanding the requirements. ### What a Community App Costs A focused first release — chat or a feed, profiles, notifications, and the moderation baseline the stores require — is a Business-tier build: $30–60K, over three to five months. A platform at Jepta's width — chats, channels, posts, comments, and a location-aware feed in one release — or one with heavy media pipelines and operational moderation tooling runs toward enterprise work: from $90K+. These are the same published tiers as on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page — a vertical does not get a second price list. What moves a community number within them: how many real-time surfaces ship in v1, the media pipeline (text is cheap, video is not), and how much moderation tooling must exist operationally on day one. ### Frequently Asked Questions Common questions from teams building social and community products on Flutter. #### Is Flutter good for social and community apps? Yes. Chat lists, feeds, and media-heavy screens are custom, animation-dense UI — exactly what Flutter renders identically on iOS and Android from one codebase, compiled to native code so conversations stay at 60 frames per second even while messages stream in. Just as important is the launch math: a community that ships on one platform has cut its network effect in half, and one codebase makes launching on both stores the default rather than a doubled budget. We shipped Jepta, a full neighborhood platform, exactly this way. #### How much does it cost to build a community app? A focused first release with chat or a feed, profiles, notifications, and the moderation baseline the stores require is a Business-tier build over three to five months; a platform as wide as Jepta — chats, channels, posts, comments, and a location-aware feed in one release — runs toward enterprise work. The ranges are published on this page and the Flutter app development page. What moves the number most is the count of real-time surfaces in the first release and whether the media pipeline includes video. #### How do you build real-time chat in a Flutter app? As one shared real-time layer, not as a feature bolted per screen: a WebSocket transport with one reconnection policy, one message-ordering rule, and one answer to what happens when the connection drops mid-conversation. The rendering side is its own discipline — scoping rebuilds so a new message updates one cell rather than the whole list — and we have pushed it as far as a chat holding thousands of streaming markdown messages at 60 fps. Built once, that layer serves chat, presence, reactions, and live counts alike, which is why Jepta's later screens were cheap. #### What does Apple require from an app with user-generated content? Guideline 1.2, enforced at review: a method for filtering objectionable content, a mechanism for users to report it, the ability to block abusive users, and published terms of service. An app missing any of these can be rejected regardless of quality, and the machinery also has to work operationally — reports reaching a human, blocks severing every surface where two users could meet. We scope moderation with the first release, because retrofitting a block system across chats, comments, and feeds later means rewriting the social graph's edges. #### Can a Flutter community app scale to many users? The client scales if the rendering is disciplined; the real scaling questions live on the transport and the backend. What the server may push and how often, how updates are coalesced under load, how a feed's ordering is computed at the query level, and how fan-out is handled when one post reaches thousands of devices — those decisions, not the UI framework, decide the ceiling. We have engineered exactly that pattern under real-time market data as well as community traffic, and it transfers whole. #### Do we launch with apps on both iOS and Android? For a community product, almost always yes, and more strongly than for any other category: your users invite each other, and every invitation that crosses a platform boundary fails if you shipped to one store. A single Flutter codebase produces both apps, so the marginal cost of the second store is small — which converts a structural risk into a line item. The exception is a deliberately closed beta community, where one platform can be a sequencing choice rather than a launch strategy. Read the [Jepta case study](https://nerdy.pro/portfolio/jepta) for the full six-month build, the [Arcana case study](https://nerdy.pro/portfolio/arcana) for chat rendering at its hardest, or start from the [Flutter app development](https://nerdy.pro/services/flutter-app-development) pillar. ## Web Development https://nerdy.pro/services/web-development ### Modern Web Development with Nuxt and SSR We build fast, search-friendly web applications on a modern, server-rendered stack. Not a page-builder, not a bloated single-page app that ships a blank screen to Google — real applications that render on the server, load quickly on a phone, and rank. How sure are we about this stack? **This very site is built on it** — Nuxt 4, server-side rendering, Tailwind CSS, and Nuxt Content. We didn't pick these tools for a sales page; we run our own business on them. We've also shipped them for clients, including [Formtastic](https://nerdy.pro/portfolio/formtastic), a form-building platform built on Nuxt, and the [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform), whose Nuxt admin back office launches branded apps on top of PHP services. - **SSR** Indexable by Default: Server-rendered HTML crawlers can read without running JavaScript - **90+** Lighthouse Targets: We build to Core Web Vitals, not against them - **Mobile-First** Responsive Everywhere: Designed for phones first, scaled up to desktop - **Type-Safe** TypeScript End to End: From the API layer to the component props ### Our Stack — and Why Nuxt + SSR The framework choice is the difference between a site Google can read instantly and one it struggles to render. Here is what we build on and why. #### Server-Side Rendering for SEO A traditional single-page app ships an empty HTML shell and asks the browser to build the page with JavaScript. Crawlers, link previews, and slow devices all suffer for it. Nuxt renders the full HTML on the server, so the content is present on the very first response — meta tags, headings, structured data, and copy all there before a single line of client JavaScript runs. This is the single biggest on-page SEO lever a framework can give you, and it's the default, not an add-on. #### Tailwind for Fast, Consistent UI We style with Tailwind CSS — utility-first, no wandering global stylesheets, no dead CSS shipped to users. It keeps the design system consistent across every page, makes responsive breakpoints explicit, and ships only the classes you actually use. The result is a UI that's quick to build, easy to maintain, and small to download. #### Nuxt Content for Content-Driven Sites For marketing sites, documentation, and blogs, [Nuxt Content](https://content.nuxt.com) turns Markdown into fully rendered, server-side pages with type-safe schemas — exactly how this site's blog and service pages work. Editors write Markdown; the site stays fast and indexable; developers keep version control over everything. No database required. #### Strapi When You Need a Headless CMS When non-technical editors need to own the content — products, listings, campaigns — we wire up [Strapi](https://strapi.io), an open-source headless CMS. Your team gets a friendly admin panel; the Nuxt frontend consumes a clean API and renders it server-side. You get editorial freedom without giving up performance or SEO, and you own the data and the hosting. #### What We Get Right - Mobile-Compatible Pages: Mobile-first, responsive layouts that work on every screen from phone to widescreen — tested, not assumed - Authentication Done Properly: Sessions, OAuth, and JWT with real server-side authorization checks — not a login screen with an open back door - Secure Cookies & Consent: httpOnly, secure, SameSite cookies for sessions, plus GDPR-compliant consent handling for analytics and tracking - SEO Built In: Server-rendered meta tags, Open Graph, canonical and hreflang links, and JSON-LD structured data on every page - Performance & Core Web Vitals: Image optimization, code splitting, and caching tuned to pass Core Web Vitals on real devices - Headless CMS Integration: Nuxt Content or Strapi so your team can publish without touching code > **Security is part of the build** > > Authentication, cookies, and data handling are where most web apps quietly leak. We build them right from the start — and if you've inherited an app you're unsure about, our [AI-generated code audit](https://nerdy.pro/services/ai-code-audit) reviews authentication, session security, and data exposure before they become an incident. ### What We Build Four kinds of web work come up again and again in our engagements, and each now has its own page with the proof behind it: - **[Admin panels & dashboards](https://nerdy.pro/services/web-development/admin-dashboards)** — the back offices behind products: role-based access, forms that drive real machinery, tables that stay usable at scale. Anchored by the admin UI that launches branded apps for our [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform). - **[Nuxt development](https://nerdy.pro/services/web-development/nuxt-development)** — marketing sites, content platforms, and migrations onto server-side rendering, like [Formtastic's](https://nerdy.pro/portfolio/formtastic) move from Django templates to Nuxt. This site is the standing demo. - **[Companion web apps](https://nerdy.pro/services/web-development/companion-web-app)** — the browser surface of a product that lives primarily in a mobile app, sharing one API and one backend with it. - **[Embeddable widgets & scripts](https://nerdy.pro/services/web-development/embeddable-widgets)** — JavaScript that runs inside pages you don't control, built dependency-free and defensive, like the collector behind our [bot-detection platform](https://nerdy.pro/portfolio/realtime-bot-detection-platform). ### Flutter for the Web — Pros and Cons Flutter can compile to the web, and clients with a Flutter mobile app often ask whether they should reuse it for their website too. The honest answer: sometimes — but rarely for public, content-driven web. Here's the trade-off. | Aspect | Nuxt + SSR (Public Web) | Flutter Web | Traditional SPA | | --- | --- | --- | --- | | SEO / Indexability | Excellent (SSR) | Poor (canvas render) | Weak (empty shell) | | Initial Load Size | Small | Heavy (large bundle) | Medium | | Code Shared with Mobile | None | Near 100% | Some (React Native) | | App-Like / Complex UI | Good | Excellent | Good | | Content / Marketing Sites | Ideal | Poor Fit | Workable | | Accessibility | Native HTML | Improving, Limited | Native HTML | | Best For | Sites & Web Apps | Internal / App-Like Tools | Dashboards | **Where Flutter web shines:** an app-like product where you already have a Flutter mobile codebase and want one team shipping everywhere — internal dashboards, logged-in tools, highly interactive or canvas-driven UIs behind a login wall, where SEO doesn't matter and users expect an app, not a page. **Where it hurts:** anything that needs to be found on Google. Flutter web renders to a canvas, which crawlers struggle to read, ships a heavy initial bundle, and fights the browser on accessibility and deep-linking. For marketing sites, blogs, e-commerce, and landing pages, that's disqualifying. > **Bottom line:** Use **Nuxt + SSR** for anything public or SEO-driven — marketing sites, content, e-commerce, SaaS marketing surfaces. Use **Flutter web** when you're extending a Flutter mobile app into an app-like web tool where reach through search isn't the goal. We build both and will tell you honestly which one your project needs. If mobile is also on the table, see our [Flutter app development](https://nerdy.pro/services/flutter-app-development) service and the trade-offs in [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026) and [Flutter vs Native](https://nerdy.pro/blog/flutter-vs-native-2026). ### Web Development Pricing Transparent ranges based on scope and complexity #### Marketing / Landing Site Fast, SEO-ready, content-driven **$5–12K** 2-4 weeks - Nuxt + SSR for full indexability - Responsive, mobile-first design - SEO meta, Open Graph & structured data - Nuxt Content or Markdown CMS - Contact forms & analytics - Core Web Vitals tuned - Deployment & domain setup [Get Started](https://nerdy.pro/contact?intent=web-development) #### Web App (Popular) Authenticated, interactive product **$15–40K** 1-3 months - Everything in Marketing Site - User authentication (OAuth / JWT) - Secure sessions & cookie handling - REST or GraphQL API integration - Role-based access control - Database design & integration - Admin dashboard [Get Started](https://nerdy.pro/contact?intent=web-development) #### Headless CMS Platform Editor-owned content at scale **$20–50K** 2-4 months - Strapi or Nuxt Content backend - Custom editorial admin panel - Multi-language content support - Server-rendered, indexable pages - Media & asset management - Preview & publishing workflow - API-first architecture [Get Started](https://nerdy.pro/contact?intent=web-development) #### SaaS / E-commerce Complex, scalable web product **$50K+** 4+ months - All platforms & integrations - Payments & subscriptions - Multi-tenant architecture - Advanced security & compliance - Scalable backend infrastructure - Monitoring & observability - Dedicated support [Get Started](https://nerdy.pro/contact?intent=web-development) > **Custom Pricing Available** > > Every project is unique. These ranges are approximate and based on typical scope. We'll provide a detailed quote after understanding your requirements, integrations, and timeline. Contact us for a free consultation and an accurate estimate. ### Frequently Asked Questions Common questions about our web development services #### Why do you build with Nuxt instead of WordPress or a website builder? Nuxt gives you server-side rendering, which means search engines and link previews see fully built HTML on the first request rather than an empty shell. You get the speed and indexability of a static site with the power of a real application, full version control over the code, and no plugin bloat or security surface of a typical WordPress install. For content-heavy sites we pair Nuxt with a headless CMS so editors still get a friendly admin panel. #### Does server-side rendering actually improve SEO? Yes, significantly. With SSR the page content, meta tags, Open Graph data, and structured data are present in the HTML the server sends, so crawlers can read them without executing JavaScript. Single-page apps that render entirely in the browser are slower to index and can be missed entirely by some crawlers and social link previews. SSR is the most impactful framework-level decision you can make for organic search. #### Can you build my web app with Flutter for the web? We can, and for the right project it is a great fit. Flutter web works well for app-like products and internal tools behind a login, especially when you already have a Flutter mobile app and want to share one codebase. It is a poor fit for anything that needs to rank on Google, because it renders to a canvas that crawlers struggle to read and ships a heavy initial bundle. For public, content, or e-commerce sites we recommend Nuxt with server-side rendering instead. #### How do you handle authentication and user data securely? We implement real server-side authorization, not just a login screen. Sessions use httpOnly, secure, SameSite cookies so tokens cannot be stolen by client-side scripts, and we support OAuth and JWT flows with role-based access control. Cookie consent is handled to meet GDPR. If you have an existing app, our AI code audit service reviews authentication, session handling, and data exposure as a dedicated check. #### Should I use Nuxt Content or Strapi for my content? It depends on who edits the content. Nuxt Content is ideal when developers or technical writers manage content in Markdown under version control, like a blog or documentation, with no database needed. Strapi is the right choice when non-technical editors need a friendly admin panel to manage products, listings, or campaigns. Both render server-side through the Nuxt frontend, so either way the site stays fast and indexable. We help you pick based on your team. #### How much does a web development project cost? A fast marketing or content site typically runs 5,000 to 12,000 US dollars, an authenticated web app 15,000 to 40,000, a headless CMS platform 20,000 to 50,000, and a complex SaaS or e-commerce product upward of 50,000. The final number depends on the number of features, integrations, and the design scope. We provide a fixed quote after a free consultation. :: ## White-Label App Development https://nerdy.pro/services/whitelabel-app-development ### White-Label Mobile App Development A white-label app is one product, sold many times — the same application, rebranded and republished for each of your partners, franchisees, or markets. The hard part isn't building one app. It's building one codebase that can become ten, fifty, or a hundred branded apps without your maintenance cost growing with every launch. We design and build white-label platforms where each new branded app is a configuration, not a fork — so a single team ships and maintains the entire fleet. - **1** Codebase: Every branded app shares the same source - **Hours** To Launch a Brand: Not weeks of engineering per app - **Unlimited** Branded Apps: Marginal cost approaches zero - **1** Fix → All Apps: One change reaches the whole fleet ### Why Flutter Is the Best Choice for White-Label Apps White-label is the use case Flutter was made for. Building a fleet of branded apps natively means maintaining two codebases (Swift and Kotlin) multiplied by every brand — an impossible maintenance burden. Flutter collapses that into **one codebase, one team, every platform**. It's the same foundation behind our [Flutter app development](https://nerdy.pro/services/flutter-app-development) work, scaled from one app to an entire fleet. #### What Flutter Gives a White-Label Fleet - Single Source of Truth: iOS and Android for every brand built from one Dart codebase — no per-platform, per-brand divergence - Pixel-Perfect Theming: Flutter renders every pixel itself, so a brand theme applies identically across all devices and platforms - Native Performance: Compiled to native ARM code — every branded app feels first-class, not like a wrapper - One Fix, Whole Fleet: A bug fixed or a feature shipped once lands in every app on the next release > **Bottom line:** For a single app, Flutter is an excellent choice. For a fleet of white-label apps, it's the only choice that keeps maintenance sane — one codebase instead of (2 platforms × N brands) projects to keep in sync. ### A Flexible Styling & Feature-Gating System The core of a white-label platform is the configuration layer that turns one codebase into many distinct apps. We build a system where everything that defines a brand lives in configuration, separate from the code. **Flexible styling**: Colors, typography, logos, app icons, splash screens, and light/dark brightness are all driven per brand. Non-technical managers set a brand's look and feel through an admin UI — no developer, no code change, no rebuild request. **Feature gating**: Not every brand ships with the same capabilities. Our feature-flag system enables or disables functionality per app — premium brands unlock advanced modules, regional apps toggle market-specific features, and pilot brands get early access — all from configuration. #### Configured Per Brand, Not Coded Per Brand - Theme & Branding: Colors, fonts, icons, splash, and brightness set per app - Feature Flags: Turn capabilities on or off for each brand independently - Store Identity: App name, bundle identifiers, and store metadata per listing - Payments & Integrations: Per-brand payment, API, and third-party configuration ### Build & Deployment Automation Fifteen apps means fifteen builds, fifteen signing setups, and fifteen store submissions for every release. Done by hand, that's where white-label projects collapse. We automate the entire build-and-release pipeline so shipping the whole fleet is a single, repeatable operation. We use [Fastlane](https://fastlane.tools) — the industry-standard mobile automation toolchain — to handle signing and provisioning, build and sign iOS and Android binaries, and submit each app to the [App Store](https://docs.fastlane.tools/getting-started/ios/appstore-deployment/) and [Google Play](https://docs.fastlane.tools/getting-started/android/release-deployment/) with its metadata and screenshots. The native build configuration each brand needs — application IDs, bundle identifiers, version numbers, signing, icons — is generated automatically from the brand's config, so no per-app settings are ever edited by hand. #### Automated From Form to Store - Per-Brand Native Config: Android & iOS build settings generated from configuration, never hand-edited - Signed Builds: Automated signing and provisioning across the whole fleet - Store Submission: Fastlane uploads binaries, metadata, and screenshots to both stores - Repeatable Releases: Every new brand inherits the same battle-tested deploy path ### Web-Based Admin UI The system is operated through a web-based admin UI we build with **Nuxt**. It's where a manager creates a new branded app — entering the brand name, uploading assets, choosing the theme and brightness, selecting which features are enabled, and filling in store metadata. Submitting the form generates the brand's configuration and kicks off the automated build pipeline. The result: launching a new branded app becomes a **self-service action for non-technical staff**, not an engineering ticket. The business scales the number of published apps without scaling the size of the engineering team. Back offices like this one are their own discipline — our [admin panels & dashboards](https://nerdy.pro/services/web-development/admin-dashboards) page covers how we build them. ### Publishing Models: Your Accounts, Your Choice How your apps appear in the stores is a strategic decision, and we support whichever model fits your business: | Publishing Model | Single Developer Account | Per-App Account | Flexible / Mixed (Most Common) | | --- | --- | --- | --- | | Store Presence | All apps under your account | Each brand on its own account | Mix per brand as needed | | Brand Separation | Apps tied to one publisher | Full per-brand separation | Separate where it matters | | Setup Overhead | Lowest — one account | Highest — account per app | Balanced | | Best For | One company, many sub-brands | Independent partner brands | Growing partner networks | > **Not Sure Which Model?** > > The pipeline supports all three, and you can change your mind later — apps can be published under a single developer account, a dedicated account per app, or any flexible mix. We'll help you choose based on brand independence, partner agreements, and store policy. > We weren't after a single app — we needed a whole family of branded apps, a new one for every partner we onboard. Building and maintaining each of those natively would have buried us in engineering cost before we ever broke even. The whole model only works if launching another branded app is nearly free. > > Nerdy Production gave us exactly that: one Flutter codebase that becomes any number of fully branded apps, plus an admin panel where our own team spins up a new brand without touching code. A single fix lands in every app, new partners go live in hours instead of weeks, and the cost of the next brand is close to zero. They turned something that felt impossible into a repeatable process — and let us grow the number of apps without growing the team. *[Evgeny Syrtsov](https://www.linkedin.com/in/evgeny-syrtsov/), CEO, Netgineers GmbH* ### White-Label App Development Pricing Transparent pricing for building and running a white-label platform #### Starter Platform One app, built to scale **$30–55K** 1-2 months - Flutter app (iOS & Android) - Theming & branding system - Feature-flag foundation - Build & deploy automation - Single-account publishing - Up to 2 launch brands [Get Started](https://nerdy.pro/contact?intent=whitelabel-app-development) #### Fleet Platform (Popular) Built for many brands **$55–100K** 2-3 months - Everything in Starter - Web-based admin UI (Nuxt) - Self-service app creation - Automated store submission - Flexible publishing models - Seller / partner app option [Get Started](https://nerdy.pro/contact?intent=whitelabel-app-development) #### Enterprise Fleet Large-scale & custom **$100K+** 3+ months - Everything in Fleet - Custom backend & multi-tenant API - Advanced feature gating - Per-app developer accounts - Analytics across the fleet - Dedicated support & SLAs [Get Started](https://nerdy.pro/contact?intent=whitelabel-app-development) > **Custom Pricing Available** > > Every white-label platform is different. These ranges are approximate — we'll provide a detailed quote after understanding how many brands you plan to launch, your feature set, and your publishing strategy. Contact us for a free consultation. ### Frequently Asked Questions Everything you need to know about white-label app development #### What is a white-label app? A white-label app is a single application that can be rebranded and republished as many separate, independently branded apps. Each one has its own name, logo, colors, and store listing, but they all run on the same shared codebase — so you build and maintain one product while shipping many branded apps. #### Why is Flutter the best choice for white-label apps? Building white-label apps natively would mean maintaining two codebases (Swift and Kotlin) for every single brand. Flutter lets you build iOS and Android for every brand from one Dart codebase, with pixel-perfect theming and native performance. One fix or feature reaches the entire fleet on the next release — making it the only approach that keeps maintenance manageable at scale. #### How do new branded apps get created and launched? Through a web-based admin UI we build with Nuxt. A non-technical manager enters the brand name, uploads assets, picks the theme, brightness, and enabled features, and fills in store metadata. Submitting the form generates the brand configuration and triggers an automated build-and-deploy pipeline (powered by Fastlane) that produces signed apps and submits them to the App Store and Google Play. #### Can each brand have different features? Yes. We build a flexible feature-gating system so capabilities can be turned on or off per brand from configuration. Premium brands can unlock advanced modules, regional apps can toggle market-specific features, and pilot brands can get early access — all without changing the shared code. #### Can apps be published under separate developer accounts? Yes — apps can be published under a single developer account, a dedicated account per app, or any flexible mix. The publishing model is your choice based on brand independence, partner agreements, and store policy, and the automated pipeline supports all three. Retail and loyalty fleets are the most common shape of this work — the [cashback loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform) case study shows the architecture live, and our [e-commerce app development](https://nerdy.pro/services/flutter-app-development/e-commerce) page covers the commerce side the fleet plugs into. ## Arcana https://nerdy.pro/portfolio/arcana ### Project Overview Arcana is an AI-powered tarot companion that guides users through card readings via a conversational chat interface. The AI backend — responsible for the tarot knowledge and interpretation logic — was developed by the client's team. Our responsibility was the Flutter mobile client: a performant, polished chat experience on both iOS and Android. The nature of the product created an immediate technical challenge. Chat sessions grow long. Users return daily, accumulate readings, and the message history can reach thousands of entries. At the same time, the AI streams its responses in real time — outputting formatted text that includes bold interpretations, italicized card names, headers, and bullet-point breakdowns. Building a chat screen that handles both of these gracefully — at 60 fps on budget hardware — was the core of our work. ![Daily Card Draw](https://nerdy.pro/projects/arcana/images/01.webp) ![Streaming Chat Response](https://nerdy.pro/projects/arcana/images/02.webp) ![Card Spread Reading](https://nerdy.pro/projects/arcana/images/03.webp) ### Real-Time Streaming via SSE The AI responses arrive as a stream of tokens, not a single payload. We implemented a Server-Sent Events (SSE) client in Flutter that opens a persistent HTTP connection to the backend and processes the token stream as it arrives. Each incoming chunk is appended to the active message in the chat, giving users the familiar "typing" effect — watching the interpretation appear word by word. Handling SSE in Flutter required careful management of the connection lifecycle: reconnecting on network interruptions without losing partial content, buffering incomplete UTF-8 sequences at chunk boundaries, and ensuring the UI rebuilds only the affected message widget rather than the entire list on each token arrival. ### On-the-Fly Markdown Rendering Because the AI formats its responses in Markdown — using bold for card names, italics for keywords, headers for spread sections, and lists for key points — the chat renderer needed to parse and display Markdown incrementally, updating in real time as new tokens stream in. We built a custom incremental Markdown renderer that processes the growing string on each update without re-parsing the entire message from scratch. Partial tokens at the end of the current buffer (an unclosed `**` or a half-written heading) are held in a pending state and rendered as plain text until the delimiter is resolved. This avoids flicker and layout jank while maintaining correct formatting as the response completes. ### Performance Benchmarking on Low-End Devices Long chat histories are a given for a daily-use app. We designed the system to handle 5,000+ messages without degradation, and we validated this with structured benchmarks on low-tier Android devices — phones representative of the budget segment where Flutter performance pressure is highest. Our benchmark suite scrolled through chat lists of varying sizes (500, 2,000, and 5,000 messages) while simultaneously streaming an active SSE response, measuring frame times throughout. Initial builds showed frame drops during fast scrolling at the 2,000+ message mark as layout passes for Markdown-rendered bubbles became expensive. We addressed this with several targeted optimizations: **Lazy layout caching**: Rendered Markdown layouts are cached by message ID. Once a message bubble has been laid out, its dimensions and widget tree are reused on subsequent scroll passes rather than recomputed. **Sliver-based virtualization**: The chat list uses a `SliverList` with a custom delegate that avoids building off-screen widgets. Only messages within or near the visible viewport are instantiated, keeping the widget tree shallow regardless of total message count. **Isolated stream rebuilds**: The streaming message at the bottom of the list is managed independently from the historical list. SSE token updates trigger a targeted rebuild of only the active bubble widget, leaving the virtualized list completely untouched. After these optimizations, frame time stayed consistently below 16 ms across all benchmark scenarios — including 5,000-message histories with an active stream — on the target low-tier devices. ### Results The final client delivers a chat experience that feels fluid and responsive at any history depth, on any device in the target range. Streaming responses render smoothly in real time with correct Markdown formatting throughout, and long-term users accumulating hundreds of sessions never encounter slowdowns. The performance foundation we established gives the product room to grow without revisiting the rendering architecture. Arcana's client was built as part of our [Flutter app development](https://nerdy.pro/services/flutter-app-development) work, where rendering performance like this is a core focus, and it stands as the hardest chat-rendering case behind our [social & community app development](https://nerdy.pro/services/flutter-app-development/social) practice. ## Cashback & Loyalty App Platform https://nerdy.pro/portfolio/cashback-loyalty-platform ### Project Overview Our client runs a cashback platform that lets offline shops — cafés, salons, boutiques, neighbourhood retail — reward their customers and, just as importantly, finally get to know them. Brick-and-mortar businesses rarely have the customer data that online stores take for granted: who their regulars are, when they last visited, what brings them back. The platform closes that gap by putting a cashback and loyalty program in every participating shop's hands, and a branded app in every customer's pocket. Rather than ship a single app, the business model calls for a *family* of applications: a flagship consumer app where shoppers discover participating shops and track their cashback, a dedicated merchant app for shops to connect and run their own rewards, and a growing catalogue of white-label apps — each one a fully branded, independently published cashback app for a partner brand or retail chain, all powered by the same product. By the time the project matured, the ecosystem spanned **15+ live mobile applications**. The engineering challenge was never building one app well — it was building one codebase that could become any number of branded cashback apps without the maintenance cost growing linearly with each new partner. We were responsible for the entire system end to end: the shared consumer app, the merchant app, the white-label fleet, the cashback rules engine and backend services, and the internal admin tooling that turned "launch a new branded app" from a multi-day engineering task into a self-service form. > This project is covered by a non-disclosure agreement. The client, the partner brands behind the white-label apps, and the published store listings are not named — the screenshots below show the product running on test data. ![Consumer app home with cashback balance and invite-friends bonus](https://nerdy.pro/projects/cashback-loyalty-platform/images/01.webp) ![Shop details with per-purchase cashback rate](https://nerdy.pro/projects/cashback-loyalty-platform/images/02.webp) ![Vouchers and shop news feed](https://nerdy.pro/projects/cashback-loyalty-platform/images/03.webp) ![Single white-label application setup](https://nerdy.pro/projects/cashback-loyalty-platform/images/05.webp) ![Admin UI white-label applications list](https://nerdy.pro/projects/cashback-loyalty-platform/images/06.webp) ### One Codebase, Many Apps The flagship consumer app and every white-label variant run on a **single codebase**. There are no forks, no per-brand branches, and no copy-pasted projects to keep in sync. What differs between apps — branding, theme, brand identity, feature flags, reward configuration, and store-listing metadata — is expressed entirely as configuration that lives outside the code. We built the client around a flavor-and-configuration system: a single binary blueprint that resolves its brand identity at build time from a per-app config bundle. Colors, typography, logos, splash screens, app icons, bundle identifiers, and enabled feature sets are all injected per flavor. A bug fixed once, or a feature shipped once, lands in every app in the fleet on the next release — there is no per-brand divergence to maintain. Crucially, the levers that define each brand are operated by **managers with no programming skills**. Through the admin UI, a non-technical operator sets a new app's **icons, colors, brightness (light or dark theme), and select labels** — and those choices flow straight into the build with no developer involvement. The people who own a brand's look and feel can change it directly, instead of filing a ticket and waiting on engineering. This is the core economic lever of the project: the marginal cost of the sixteenth app is close to zero, because it is the same code as the first, with a different configuration. ### Native Build Configuration in Dart A white-label fleet lives or dies on the native build configuration — the Android and iOS settings that the stores actually care about. Application IDs and bundle identifiers, app display names, version and build numbers, signing setup, icon and splash assets, and per-flavor entries in `build.gradle` and the Xcode project all have to be correct for every single app, every single build. Rather than maintain that by hand, we wrote **custom Dart tooling that programmatically manages the Android- and iOS-related build configuration**. Given a brand's config bundle, the tool rewrites the relevant Gradle, manifest, `Info.plist`, and asset-catalogue entries to match the target flavor before the build runs. Keeping this logic in Dart means it shares the same source of truth as the Flutter client and runs as a first-class step of our build process — so a brand's native configuration is generated deterministically from its config, never edited by hand. ### The Merchant App Offline shops needed their own tool — not a stripped-down consumer view, but a purpose-built app for running a rewards program and understanding their customers. From the merchant app, a shop owner connects to the platform in minutes, defines their cashback and discount rules, confirms customer purchases to grant cashback, and finally sees who their customers are: new versus returning, how often they visit, and how each promotion performs. For many of these businesses it is the first structured view of their customer base they have ever had. We built it on the same Flutter foundation and shared component library as the consumer apps, so design-system updates and platform fixes propagate across both audiences, while the merchant-specific workflows live in their own modules. ### Configurable Cashback & Discount Rules The heart of the platform is a flexible **rules engine** that lets each shop design its own rewards with no help from us. Through the merchant app, a shop composes rules such as: - **Per-purchase cashback** — a percentage or fixed amount returned to the customer's balance on every qualifying purchase. - **Birthday bonus** — a reward credited automatically around a customer's birthday to bring them back through the door. - **Invitation bonus** — a referral reward that pays out when an existing customer invites a friend who joins and makes their first purchase. Rules are data, not code: each shop picks its own combination, sets the amounts, caps, and validity windows, and the engine evaluates them at purchase time. Because the rules live in configuration, a shop can launch a birthday campaign or change a cashback rate itself, instantly, across its app — and we never ship a release to support it. ### The Admin UI & Automated Build Pipeline The standout piece of the system is the internal admin UI. Onboarding a new white-label brand used to be an engineering errand — create the configuration, register the bundle identifiers, generate signing assets, set up the store listings, and add the new target to CI. We collapsed all of that into a single web application that managers operate without writing code. A manager fills in a form — brand name, theme, assets, store metadata, feature selection — and the system does the rest: **Configuration generation**: The admin UI persists the new brand's configuration and assets and produces the per-flavor config bundle the Flutter client consumes. **Pipeline registration**: The new app target is registered into the CI/CD pipeline automatically. The build system picks up the new flavor, runs our custom Dart tooling to apply the native Android and iOS configuration, and produces signed artifacts for both platforms. **Self-service launches**: What previously required a developer in the loop for every new brand became a form-driven, repeatable process. The business can scale the number of published apps without scaling the size of the engineering team. ### Build & Deploy Automation with Fastlane The entire build-and-release process is automated end to end with [Fastlane](https://fastlane.tools) — the open-source toolchain for automating mobile build and deployment. We use it to standardize every step that would otherwise be manual and error-prone across 15+ apps: managing signing and provisioning, building and signing the iOS and Android binaries, and submitting each app to the [App Store](https://docs.fastlane.tools/getting-started/ios/appstore-deployment/) and [Google Play](https://docs.fastlane.tools/getting-started/android/release-deployment/) along with its metadata and screenshots. Because every app shares one Fastlane configuration parameterized per flavor, releasing the whole fleet is a single, repeatable operation. A new white-label brand inherits the exact same battle-tested deploy path as the flagship app — no bespoke release scripting required. The admin UI is a Nuxt application backed by our PHP services and a PostgreSQL data store, with Ruby and Fastlane tooling orchestrating the build-and-release automation. ### Backend & Services Underpinning every app is a shared backend that runs the **cashback ledger**, the rules engine, customer profiles, and per-brand configuration. A single set of services backs the entire fleet, with brand context resolved per request — so a customer in any white-label app, the flagship app, or a shop using the merchant app is talking to the same API surface, scoped to their brand. The ledger records every accrual and redemption so balances are always auditable, and customer profiles give each shop the insight into their audience that offline retail usually lacks. ### Deferred Deep Linking & the Invitation Bonus The feature this unlocked was the **invitation bonus**. Referrals are how the platform grows: a happy customer invites a friend, who taps the invite link — but almost never has the app installed yet. With a plain deep link, that invite context was lost the moment the friend hit the store: they would install, open the app, land on a generic home screen, and the connection between inviter and invitee was gone — and with it the invitation bonus both sides were promised. The most viral moment in the product was broken at its most important step. We fixed it with **deferred deep linking** so the invitation survives the install. Because the whole fleet shares one codebase, the feature was built once and every branded app — flagship, merchant, and all white-label variants — inherited it automatically. We implemented it with first-party mechanisms rather than a third-party service: a lightweight **iOS App Clip** captures the invite link before the full app exists and hands it off through a shared App Group container, while the **Google Play Install Referrer API** carries the payload on Android. A pending-link queue holds the invitation until the app is ready and the new customer has signed up and logged in — only then is the referral attributed and the invitation bonus credited to both the inviter and the new customer, so the reward resolves correctly even though it spans an install and a fresh login on a cold start. We wrote up the full technical approach — including why the common solution stopped working — in [Deferred Deep Linking After Firebase Dynamic Links: App Clips + Play Install Referrer](https://nerdy.pro/blog/deferred-deep-linking-app-clips-install-referrer). ### Results The client can now launch a fully branded, store-ready cashback app for a new partner in a fraction of the time it once took — without pulling engineers off product work for each launch. The shared-codebase architecture keeps 15+ apps in lockstep: one fix, one feature, one release reaches them all. The admin UI turned a recurring engineering bottleneck into a self-service capability, and the automated pipeline made each new launch a predictable, repeatable event rather than a bespoke project. And the offline shops that once flew blind now run their own cashback and discount campaigns and, for the first time, know who their customers are. The economics in numbers: - **15+ live apps from one codebase** — no forks, no per-brand branches, no divergence to reconcile at release time. - **The marginal cost of the next branded app is close to zero** — a new brand is a configuration bundle plus store metadata, not a new engineering project. - **Launching a brand went from a multi-day engineering task to a self-service form** operated by managers with no programming skills. - **The single Flutter codebase runs about 30–40% cheaper than two native apps would** — and because the fleet shares it, that saving is avoided once per brand, compounding across all 15+ apps. This is the kind of platform we build as a [white-label app development](https://nerdy.pro/services/whitelabel-app-development) engagement — one Flutter codebase powering an entire fleet of branded apps — and it anchors the loyalty-and-cashback side of our [e-commerce app development](https://nerdy.pro/services/flutter-app-development/e-commerce) practice. The admin UI that launches each brand is the kind of internal back office our [admin panels & dashboards](https://nerdy.pro/services/web-development/admin-dashboards) page describes. ### FAQ #### What is a white-label app platform? A single codebase that can be rebranded and republished as any number of separate, independently branded apps. On this platform, the flagship cashback app and every partner app share the same Flutter code; what differs — colors, logos, app icons, bundle identifiers, feature flags, reward configuration, store metadata — is expressed entirely as per-brand configuration that lives outside the code. #### How can one Flutter codebase power more than 15 different apps? Through a flavor-and-configuration system: a single binary blueprint resolves its brand identity at build time from a per-app config bundle, and custom Dart tooling rewrites the native Android and iOS build configuration — Gradle entries, manifests, Info.plist, asset catalogues — to match the target brand before each build. A bug fixed once, or a feature shipped once, lands in every app in the fleet on the next release. #### How does a new branded app get launched without developers? Through an internal admin UI. A manager fills in a form — brand name, theme, icons, colors, store metadata, feature selection — and the system generates the configuration bundle, registers the new app in the CI pipeline, and produces signed iOS and Android artifacts. Fastlane then submits each app to the App Store and Google Play with its metadata and screenshots, so releasing the whole fleet is a single repeatable operation. #### Do the app stores reject white-label apps? They reject fleets of thin, near-identical reskins — that is exactly what Apple guideline 4.2.6 exists for. A white-label platform has to be designed to pass review from the start: each app in this fleet is a fully branded product with its own identity, store listing, and per-brand configuration of features and rewards, not a template with a swapped logo. The publishing pipeline also supports separate developer accounts per brand where partner agreements or store policy call for it. #### What does a white-label fleet cost compared to separate apps? The first app carries the platform cost — the shared codebase, the theming and feature-flag system, the rules engine, the admin panel, and the automated pipeline. Every app after that is dramatically cheaper, because a new brand is configuration rather than a project. On top of that, the single Flutter codebase runs about 30–40% cheaper than maintaining separate native iOS and Android apps, a saving that compounds across the whole fleet. #### How does the invitation bonus survive the app not being installed yet? Through deferred deep linking built on first-party mechanisms: a lightweight iOS App Clip captures the invite link before the full app exists and hands it off through a shared App Group container, while the Google Play Install Referrer API carries the payload on Android. A pending-link queue holds the invitation until the new customer has installed, signed up, and logged in — only then is the referral attributed and the bonus credited to both sides. ## ExtraETF https://nerdy.pro/portfolio/extraetf ExtraETF is a mobile platform for ETF and stock market analysis that gives investors tools for portfolio tracking and market research. The project centered on real-time updates, easy authentication, and premium subscription features. ![Onboarding](https://nerdy.pro/projects/extraetf/images/01.webp) ![News Page](https://nerdy.pro/projects/extraetf/images/02.webp) ![Theme Page](https://nerdy.pro/projects/extraetf/images/03.webp) ![Article](https://nerdy.pro/projects/extraetf/images/04.webp) ![Equity Page](https://nerdy.pro/projects/extraetf/images/05.webp) ![Equity Details](https://nerdy.pro/projects/extraetf/images/06.webp) ### The Challenge When Isarvest GmbH approached us to build ExtraETF, they wanted a mobile application that would open up ETF investing through analytics and real-time market data. The app had to handle complex data visualization, live price updates, and premium features, and stay fast and pleasant to use on both iOS and Android. ### Our Approach We chose **Flutter** as the foundation for mobile development, so we could build once and ship to both platforms at the same time. That helped us meet a tight launch timeline without giving up code quality or feature parity. Flutter's widget system and rendering performance suited the financial charts and real-time data streams the app is built around. For real-time price updates, we implemented a **Go**-based WebSocket service that efficiently broadcasts live market data to both web and mobile clients. Go's concurrency model and lightweight goroutines manage thousands of simultaneous WebSocket connections with little resource overhead. ### Technical Wins Building features once and shipping them everywhere sped development up considerably. Hot reload shortened iteration cycles, and the single codebase removed a whole class of platform-specific inconsistencies. The Go WebSocket service held up well in production: real-time price updates reached clients with low latency even as the user base grew, and the service stayed cheap to run. Integration of native platform features—Apple and Google OAuth, push notifications, in-app purchases—went smoothly through Flutter's plugin ecosystem, so the app feels native on each platform without giving up the shared codebase. ### Business Impact The technology choices translated directly to business outcomes: - **Six months from kickoff to both stores**, with feature parity on day one — no staggered iOS-first launch and no platform waiting for the other to catch up. - **Time-to-market reduced by an estimated 40%** compared to running two parallel native builds — the number that falls out when the same work stops being paid for twice. - **Around 99% of the mobile code is shared** between iOS and Android; the platform-specific part is a few hundred lines of native glue for billing, OAuth, and push. - **One engineering team owns the entire mobile stack**, so maintenance overhead stayed flat where two native teams would each carry their own upgrade cycle. The app launched on both the App Store and Google Play and has kept strong user retention, helped by its smooth performance and real-time data. The subscription model runs on the in-app purchase integration and brings in steady revenue, and the architecture has scaled with the user base. ### Key Features #### Push Notifications Real-time market alerts and portfolio updates keep users informed about significant price movements, market news, and personalized investment insights. The notification system is built to handle high-frequency updates while maintaining battery efficiency and user preference controls. #### Authentication & Social Login Streamlined onboarding through Apple and Google OAuth integration provides users with secure, one-tap authentication. This reduces friction in the signup process and ensures account security through trusted identity providers. #### Deep Linking Universal links and app links take users from web content, email campaigns, and social media straight to specific screens in the app. Users can share ETFs, portfolios, or market analyses with each other. #### Interactive Charts Advanced charting capabilities display market data with multiple timeframes, technical indicators, and comparison tools. The charts are optimized for mobile interaction with pinch-to-zoom, gesture controls, and responsive performance even with large datasets. #### Theme Support Full dark and light themes follow user preferences and system settings, so the app stays comfortable to read in any lighting. The design system keeps both themes visually consistent and readable. #### Subscription Management In-app purchase integration for premium features with seamless subscription management through Apple App Store and Google Play Store. The system handles trial periods, subscription tiers, and restoration across devices. ### FAQ #### What is ExtraETF? ExtraETF is a mobile app for ETF and stock market analysis built for Isarvest GmbH — real-time market data, interactive charts, portfolio tracking, and premium subscription features on iOS and Android. We built the mobile side with Flutter and the real-time data layer with a Go WebSocket service. #### Why was Flutter chosen for ExtraETF? One team had to ship the same features to iOS and Android on a tight timeline, and a financial charting app needs to own its pixels — a real chart is custom rendering, not a stock UI widget. Flutter provides both: a single Dart codebase and a canvas to draw on directly. In the finished app around 99% of the mobile code is shared between the platforms, with only a few hundred lines of native glue for billing, OAuth, and push. #### How does ExtraETF handle real-time market data? A Go-based WebSocket service broadcasts live prices to web and mobile clients, using goroutines to hold thousands of simultaneous connections with little resource overhead. Updates are conflated upstream to a steady cadence of roughly one update per instrument per second — the rate a human watching a price actually needs — which keeps latency low and client CPU and bandwidth sane as the user base grows. #### How long did ExtraETF take to build? Six months from kickoff to launch on both the App Store and Google Play, with feature parity between the platforms from day one. Compared to running two parallel native builds, the estimated time-to-market reduction was about 40%, because business logic, charts, and screens were written once instead of twice. #### Can Flutter handle heavy financial charts? Yes — ExtraETF is chart-heavy by design, with multiple timeframes, technical indicators, and comparison tools. Because Flutter draws every pixel itself, the chart code runs identically on both platforms, and keeping the expensive geometry work out of the paint phase keeps interaction smooth within the 16.6ms frame budget even with large datasets. ExtraETF was built with Flutter and Go as part of our [Flutter app development](https://nerdy.pro/services/flutter-app-development) service, and it is the reference build behind our [fintech app development](https://nerdy.pro/services/flutter-app-development/fintech) practice. For the engineering deep-dive — how the Go WebSocket service fans real-time market data out to thousands of clients, and how the charts stay at 60fps in Flutter — read [How We Built a Real-Time Fintech App with Flutter and Go](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go). ## Formtastic https://nerdy.pro/portfolio/formtastic ### Project Overview Formtastic is a service for creating forms and surveys used in business scenarios with high requirements for stability, speed, and interface flexibility. As the project grew, it became clear that further development required not only updating client technologies but also targeted optimization of the server side. ![Auth Screen](https://nerdy.pro/projects/formtastic/images/01.webp) ![Main Screen](https://nerdy.pro/projects/formtastic/images/02.webp) ![Form Editor](https://nerdy.pro/projects/formtastic/images/03.webp) ![Templates List](https://nerdy.pro/projects/formtastic/images/04.webp) ![Template Editor](https://nerdy.pro/projects/formtastic/images/05.webp) ### Migration from Django to Nuxt Initially, Formtastic's frontend was implemented using Django templates. That allowed a quick start, but over time it began to limit product development. The interface grew more complex, changes required a backend developer, and the server spent too much time rendering HTML, which hurt scalability. As a result, we architecturally separated the system. Django remained as the main backend and API, while the entire user interface was moved to a separate Nuxt frontend. This provided clear separation of concerns and allowed the interface to evolve independently from server logic. After migration, UI development speed increased noticeably. The team started releasing new screens faster and improving user experience, while server load decreased as it stopped handling page rendering. Nuxt with SSR and SSG also brought clear gains in SEO and loading speed, which helped the marketing and public pages. This shape of migration — a template-rendered or single-page frontend moved onto Nuxt — is what our [Nuxt development](https://nerdy.pro/services/web-development/nuxt-development) page covers. In parallel, the server side began to evolve. In areas with increased load and strict performance requirements, part of the logic was extracted from Django and implemented in Go as separate services. This reduced latency, simplified horizontal scaling, and eliminated bottlenecks without completely reworking the backend. ### Migration from Native Apps to Flutter Formtastic's mobile clients were initially developed as two independent native applications — for iOS and Android. As the product grew, this approach became increasingly inefficient: development slowed down, features were released out of sync, and maintaining two codebases increased the total cost of ownership. The transition to Flutter allowed us to consolidate mobile development into a single codebase. This significantly simplified maintenance and made new feature releases predictable. Updates began rolling out simultaneously on both platforms, and the interface and app behavior became completely consistent for all users. From a business perspective, the effect was direct: development and maintenance costs decreased, time-to-market accelerated, and the team gained the ability to test hypotheses and develop the product faster without proportional cost increases. At the same time, Flutter allowed us to maintain high performance and quality user experience. Concretely, the consolidation changed three numbers: - **Two codebases became one.** Every feature, fix, and edge case is now written once instead of twice — the "we forgot to add this to Android" class of work disappeared entirely. - **Release drift went to zero.** Before, an iOS release could precede its Android counterpart by a week or more; now one build ships to both stores at the same time. - **The annual platform-upgrade bill roughly halved.** One Flutter upgrade cycle replaced two separate rounds of OS updates, SDK requirement bumps, and store-policy sweeps. #### iOS Application ![List of Documents](https://nerdy.pro/projects/formtastic/images/iphone_01.webp) ![Form Editor](https://nerdy.pro/projects/formtastic/images/iphone_02.webp) ![List of Forms](https://nerdy.pro/projects/formtastic/images/iphone_03.webp) ![Documents Sync](https://nerdy.pro/projects/formtastic/images/iphone_04.webp) ![Splashscreen](https://nerdy.pro/projects/formtastic/images/iphone_05.webp) #### iPad Application ![Auth Screen](https://nerdy.pro/projects/formtastic/images/ipad_01.webp) ![Main Screen](https://nerdy.pro/projects/formtastic/images/ipad_02.webp) ![Form Editor](https://nerdy.pro/projects/formtastic/images/ipad_03.webp) ![Forms Search](https://nerdy.pro/projects/formtastic/images/ipad_04.webp) ![New Document Dialog](https://nerdy.pro/projects/formtastic/images/ipad_05.webp) > For years, Formtastic shipped as two separate native apps — one for iOS, one for Android. Every feature had to be built twice, releases drifted out of sync between the platforms, and maintaining two codebases was quietly draining our roadmap. > > Nerdy Production consolidated everything into a single Flutter codebase without disrupting the product our customers depend on. Now both apps ship the same features at the same time, the experience is identical on iPhone and Android, and our time to production has dropped dramatically. We maintain one codebase instead of two, and the team finally spends its energy on new features rather than keeping two apps in sync. *[Julian Garbotz](https://www.linkedin.com/in/julian-garbotz), Managing director, Formtastic GmbH* ### Voice-to-Text for Form Fields Long free-text fields — inspection notes, incident reports, open-ended survey answers — are the slowest part of filling out a form on a phone. We added voice-to-text powered by **[Whisper](https://nerdy.pro/technologies/whisper)**, and implemented it differently on each client rather than forcing one architecture onto both. On the web app, audio is recorded in the browser and sent to a new transcription endpoint on the existing Django backend — the smaller change, since Django already owns auth and storage for that request. On the Flutter apps, Whisper is bundled directly into the binary as a GGML build running through FFI, so transcription runs on-device: no upload, no round trip, and it keeps working with no signal at all — useful on a job site, exactly where a lot of Formtastic's forms get filled out. The result is the same feature with two different trade-offs behind it. The web path stays simple because a backend was already there to extend. The mobile path trades a larger app bundle and on-device compute for dictation that works offline and never sends audio anywhere. ### Summary Formtastic's stack evolved deliberately, one bottleneck at a time. Moving the frontend from Django to Nuxt simplified web development and improved speed and SEO. Targeted use of Go on the server solved performance issues without radical platform changes. The transition from native mobile apps to Flutter reduced ownership costs and accelerated product development. As a result, the system became more stable, flexible, and better prepared for scaling. ### FAQ #### What is Formtastic? Formtastic is a German platform for creating digital forms and surveys, used in business scenarios with high demands on stability, speed, and interface flexibility. It runs as a web application plus mobile apps for iOS, iPad, and Android, with a Django and Go backend behind them. #### Why did Formtastic replace two native apps with one Flutter app? The cost of the product was no longer in building mobile but in maintaining it: two codebases, two release cycles, and two QA passes for every feature, with releases drifting out of sync between the platforms. Consolidating into a single Flutter codebase meant every feature is written once, both stores receive the same release at the same time, and the annual platform-upgrade work roughly halved. #### Did the migration to Flutter disrupt existing users? No — the consolidation happened without disrupting the product customers depend on. The Flutter apps replaced the native ones with the same features and a consistent interface on both platforms, and from that point forward updates rolled out simultaneously on iOS and Android instead of arriving on one platform first. #### How does the voice-to-text feature work without an internet connection? On the mobile apps, Whisper is bundled directly into the binary as a GGML build running through FFI, so transcription happens entirely on-device: no audio upload, no round trip, and dictation keeps working with no signal at all — useful on job sites, where many of Formtastic's forms are filled out. The web app takes the other trade-off and sends audio to a transcription endpoint on the existing Django backend. #### Why was the web frontend moved from Django templates to Nuxt? Django templates allowed a quick start but coupled every interface change to a backend developer and spent server time rendering HTML. Splitting the interface into a separate Nuxt frontend let the UI evolve independently, sped up screen development, reduced server load, and improved SEO and loading speed through server-side rendering for the public pages. Consolidating Formtastic's two native apps into one codebase is exactly the kind of migration we handle in [Flutter app development](https://nerdy.pro/services/flutter-app-development) — and running the Nuxt web application alongside those apps, on the same Django and Go backend, is the kind of work our [companion web app](https://nerdy.pro/services/web-development/companion-web-app) page describes. ## Jepta https://nerdy.pro/portfolio/jepta ### Project Overview Jepta is a hyperlocal social network built around the place you live. Instead of a global follower graph, it organizes people by proximity: chats attached to a street address or a building, group chats for the people who share it, and channels run by the bakery, the workshop or the club a few hundred meters away. Everything in the app — the chats you are offered, the channels in the feed, the posts you see first — is ranked by distance from where you are. Netgineers GmbH came to us with the product defined and the backend under way, and needed the mobile side of it built: one Flutter codebase covering iOS and Android, for a product whose surface is unusually wide for a first release. ![Onboarding](https://nerdy.pro/projects/jepta/images/01.webp) ![Location Permission](https://nerdy.pro/projects/jepta/images/02.webp) ![Chats List](https://nerdy.pro/projects/jepta/images/03.webp) ![Channel Feed](https://nerdy.pro/projects/jepta/images/04.webp) ![Channel Info](https://nerdy.pro/projects/jepta/images/05.webp) ![Channel Chat](https://nerdy.pro/projects/jepta/images/06.webp) ![New Post](https://nerdy.pro/projects/jepta/images/07.webp) ### The Challenge Two things made this more than a standard build. #### Scale of the screen surface Jepta is not one feature with settings around it. Direct chats, group chats, address chats, a channel feed, channel profiles with opening hours and photo galleries, channel-to-user conversations, a post composer with per-post comment controls, an activity tab, a profile tab, search, adding contacts by QR code, location and permission flows — all of it in the first release, in a six-month window. At that count, screens stop being independent units of work: without a shared skeleton, every new one drags its own navigation quirks, its own loading and empty states, and its own copy of the same list. #### Real-time everywhere Chat is the obvious place WebSocket connections show up, but in Jepta they are not confined to it. A channel post arriving, a comment count moving, an unread badge on the chats tab, a message marked as read — the same live connection drives all of them, across screens the user is not currently looking at. Handling that per screen would have meant a connection per screen and state that disagrees with itself the moment a user switches tabs. ### Our Approach We treated both problems as one: build the shared parts first, then let screens be thin. #### One real-time layer, many consumers The WebSocket connection lives below the UI as a single client owning its own lifecycle — connect, authenticate, heartbeat, reconnect with backoff, resubscribe. Screens do not open sockets; they subscribe to typed event streams and are handed updates. Messages sent while the connection is down are queued and flushed on reconnect rather than lost, and delivery state (sent, delivered, read) is tracked as part of the message model instead of being inferred from what happened to arrive. Because the layer is shared, a message landing in a chat the user has open and an unread badge incrementing on a tab they are not looking at are the same event handled once. #### A screen skeleton instead of screen-by-screen craft With that many surfaces, the win came from making them uniform: one navigation graph with typed routes, one State management pattern applied the same way in every feature, and a shared set of list, loading, empty and error states. New screens were mostly composition — which is what made the volume tractable inside the timeline, and what kept the app coherent rather than looking like twenty screens built by different hands. #### Location as a first-class input Proximity is not a filter bolted onto the feed, it is the product's organising principle, so the location layer had to be dependable: permission flows that degrade gracefully when a user says no, cached coordinates so the feed is never blank while a fix is acquired, and distance rendered close to where content appears ("5 km away" on a channel header) so the ranking is legible rather than mysterious. ### Results Jepta shipped to iOS and Android from a single Flutter codebase inside the six-month window, with the full feature set intact — chats, channels, posts, comments and the location-aware feed in the first release rather than staged across several. The architecture is the part that outlived the deadline. Because the real-time layer, the navigation graph and the state pattern were built once and reused, adding a screen after launch stayed a small change instead of a new integration with the socket, the router and the cache. For a product whose roadmap is "more surfaces", that is the difference that compounds. > Six months for an app this wide was the part that worried me. Jepta is neighborhood chats, business channels, posts, comments and a feed that reorders itself around wherever you happen to be standing — a long list of screens, a fixed date, and the realistic expectation that half of it would slip into a second release. > > Nerdy Production built the shared foundations first — one real-time layer, one navigation model — and after that every new screen was cheap. We launched on iOS and Android with the full feature set, chat has been dependable from day one, and extending the app since then has been a small job rather than a project. We got the product we specified, on the date we specified. *[Evgeny Syrtsov](https://www.linkedin.com/in/evgeny-syrtsov/), CEO, Netgineers GmbH* ### FAQ #### What is Jepta? Jepta is a hyperlocal communication app for German neighborhoods. It organizes people by proximity rather than by a global follower graph: chats tied to a street address or a building, group chats for the people who share one, and channels run by local businesses such as a bakery or a workshop. The chats, channels and posts a user is shown are ranked by distance from where they are. #### How do you build a Flutter app with a large number of screens? Build the shared parts first and keep the screens thin. On Jepta that meant one navigation graph with typed routes, one state management pattern applied the same way in every feature, and a shared set of list, loading, empty and error states. New screens then become mostly composition rather than new integrations, which is what keeps the volume tractable on a deadline and stops the app looking like twenty screens built by different hands. #### How do you handle WebSocket connections across many screens in Flutter? Put one connection below the UI as a single client that owns its own lifecycle: connect, authenticate, heartbeat, reconnect with backoff, resubscribe. Screens do not open sockets of their own; they subscribe to typed event streams and are handed updates. A message arriving in an open chat and an unread badge incrementing on a tab nobody is looking at are then the same event handled once, instead of one connection per screen and state that disagrees with itself. #### What happens to chat messages sent while the connection is down? They are queued and flushed on reconnect rather than lost. Delivery state — sent, delivered, read — is tracked as part of the message model instead of being inferred from whatever happened to arrive, so the interface shows the real status of a message rather than a guess about it. #### How long does it take to build a large Flutter social app? Jepta shipped to iOS and Android from a single Flutter codebase in six months, with chats, channels, posts, comments and the location-aware feed all in the first release rather than staged across several. The timeline held because the real-time layer, the navigation graph and the state pattern were built once and reused across every screen. #### Can a Flutter app rank content by location? Yes, and on Jepta proximity is the organizing principle rather than a filter applied on top of the feed. That puts real requirements on the location layer: permission flows that degrade gracefully when a user declines, cached coordinates so the feed is never blank while a fix is acquired, and the distance shown next to the content it applies to so the ranking is legible rather than mysterious. Jepta was built as part of our [Flutter app development](https://nerdy.pro/services/flutter-app-development) service, and it is the anchor build behind our [social & community app development](https://nerdy.pro/services/flutter-app-development/social) practice — the same real-time layer, told from the community angle. ## OneTwoDo https://nerdy.pro/portfolio/onetwodo ### Project Overview OneTwoDo is a local services marketplace where users post and browse listings for everyday services — cleaning, repairs, painting, plumbing, moving, beauty, and more — and connect directly with providers nearby. Our team built the product end to end: product design, the Flutter client, and everything the app runs on. ![Sign-in screen](https://nerdy.pro/projects/onetwodo/images/01.webp) ![Listings feed](https://nerdy.pro/projects/onetwodo/images/02.webp) ![Service categories](https://nerdy.pro/projects/onetwodo/images/03.webp) ![Post creation wizard — post type](https://nerdy.pro/projects/onetwodo/images/04.webp) ![Post creation wizard — localized description](https://nerdy.pro/projects/onetwodo/images/05.webp) ### Skipping the Backend Team A two-sided marketplace normally implies a backend team before it implies anything else: accounts, sessions, a database for listings and profiles, file storage for photos, and a server to put it all behind — before a single "Sign up" button can do anything real. Building and running that is usually where a small team's time and budget go before the product itself gets any attention. OneTwoDo shipped without writing that layer at all. Firebase stood in for the entire backend — Authentication for accounts and password resets, Cloud Firestore for listings and profiles, Cloud Storage (Object storage) for photos, Analytics and Crashlytics for the operational visibility a team would normally build dashboards for. None of it needed provisioning, patching, or an on-call rotation, which is what let our team carry product design, the Flutter client, and the "backend" at once, in seven weeks. ### What That Bought The saving wasn't just the initial build — it showed up every time a feature needed backend support. Multi-currency pricing across fiat and crypto, localized descriptions per listing, a feed filtered by language and location: each of these is normally a backend ticket first, a client ticket second. Here they were client-side work against a data layer that already existed, so the wizard for posting a listing — type, description, location, pricing, category, photos — could go from idea to shipped screen in the time a backend spec alone would have taken elsewhere, showing up directly in Time to market. It also lowered the product's Total cost of ownership. A launch that goes better than expected does not mean a scramble to scale a database or add a caching layer — Firebase absorbs that by default, at a cost that tracks usage instead of a fixed server bill paid whether the app has ten users or ten thousand. ### Offline Demo Mode App store review and casual demos both need a way to explore the product without a live backend or a real account. OneTwoDo ships a self-contained, in-memory data layer that swaps in for Firebase and serves seeded listings, profiles, and a working feed — the entire app is explorable end to end without touching the network, and without a reviewer or a prospect ever needing credentials. ### Results With Firebase standing in for the backend team the product would otherwise have needed, our team shipped a working cross-platform marketplace — email/password authentication with password reset, avatar upload with on-device image compression, full English/Russian localization, and an FAQ section — from an empty repository to a complete, store-ready app in seven weeks. The scoreboard for the engagement: - **Seven weeks from empty repository to store-ready app**, including product design, not just code. - **Zero custom backend services** built, provisioned, or put on call — Firebase carried accounts, data, photos, and analytics. - **One team carried all three roles** — design, Flutter client, and the managed backend — where a marketplace normally staffs each separately. - **Two locales and multi-currency pricing** (fiat and crypto) shipped inside the same seven weeks, as client-side work against a data layer that already existed. ### FAQ #### What is OneTwoDo? OneTwoDo is a local services marketplace app built for Perform Connect Studios S.L., where users post and browse listings for everyday services — cleaning, repairs, painting, plumbing, moving, beauty and more — and connect directly with providers nearby. We built the product end to end: the design, the Flutter client for iOS and Android, and everything the app runs on. #### How was a two-sided marketplace built without a backend team? Firebase stood in for the entire backend layer: Authentication for accounts and password resets, Cloud Firestore for listings and profiles, Cloud Storage for photos, and Analytics and Crashlytics for operational visibility. None of it needed provisioning, patching, or an on-call rotation, which is what let one team carry product design, the Flutter client, and the backend at once. #### How long did OneTwoDo take to build? Seven weeks from an empty repository to a complete, store-ready app — including product design, email/password authentication with password reset, multi-currency listings, English/Russian localization, and photo upload with on-device compression. Skipping the custom backend is what made that calendar possible. #### What are the trade-offs of running a marketplace on Firebase alone? The wins are speed and cost: features that would normally be a backend ticket first become client-side work against a data layer that already exists, and the bill tracks usage instead of a fixed server cost. The honest limits are vendor coupling and Firestore's query model, which rewards designing data around how screens read it. Whether that architecture fits a given marketplace is a scoping question — it fit a lean validation build like OneTwoDo exactly. #### How does the app work in demo mode without an account? OneTwoDo ships a self-contained, in-memory data layer that swaps in for Firebase and serves seeded listings, profiles, and a working feed. The entire app is explorable end to end without touching the network — so app store reviewers and prospects can walk through the product without credentials or a live backend. OneTwoDo is what our [MVP development](https://nerdy.pro/services/flutter-app-development/mvp) service looks like in practice — a first release scoped to answer one question and shipped to both stores — and it is the anchor build behind our [marketplace app development](https://nerdy.pro/services/flutter-app-development/marketplace) page, which tells the same story from the two-sided angle. ## Real-Time Bot Detection Platform https://nerdy.pro/portfolio/realtime-bot-detection-platform ### Project Overview A large share of the traffic that arrives at a commercial website is not human. Some of it is benign — search crawlers, uptime monitors, preview fetchers. The rest is not: credential stuffers, scrapers, checkout bots, and automated clicks that quietly consume advertising budgets and poison every analytics number a business makes decisions on. The problem is that all of it arrives through the same front door as real customers, and by the time it shows up in a weekly report the money is already spent. Our client set out to close that gap with a platform that answers one question — *is this visitor a person or a machine?* — while the visit is still happening. Not a nightly batch job, not a report: a verdict returned in the same moment the page loads, so that the customer's own systems can act on it immediately. We spent eight months building that product end to end, from the browser-side signal collector through the scoring services to the data platform underneath them. The engagement ran through 2022 and wrapped up in 2023. > This project is covered by a non-disclosure agreement. The client, the product name, and the customer-facing interface are omitted, and the architecture below is described at the level of shape rather than implementation detail. ### The Challenge Three constraints defined the engineering work, and each one pulled against the others. **It had to be real time.** A verdict that arrives after the visitor has already converted — or already drained a budget — is an audit trail, not a defence. Scoring had to complete inside a request, in a latency budget small enough that a customer would willingly put it in the critical path of their own page load. **It had to be high load.** The platform ran at true high-load volume, processing **tens of thousands of requests per minute** across the customer base, with traffic that spiked without warning — a campaign launch on the customer's side and an attack from the other side look identical to a capacity planner. Peak was never the average, and the system had to be sized for the peak while remaining affordable at the average. **It had to be evidential.** Every verdict had to be explainable and re-examinable after the fact. Customers do not act on an unexplained score, and neither the detection logic nor the models behind it can improve without the raw signals that produced yesterday's decisions still being on hand. That meant keeping everything — a retention and analytics problem sitting directly behind a latency problem. Real time, high volume, and total retention are three requirements that no single database satisfies. Most of the architecture below follows from refusing to compromise on any of them. ### Signal Collection in Pure JavaScript The client side of the platform is a collector that runs inside the customer's page and gathers the evidence a verdict is made from: browser and environment characteristics, rendering and timing behaviour, interaction patterns, and the many small inconsistencies that separate a real browser driven by a person from an automated one wearing a real browser's user agent. It is written in **plain JavaScript** — no framework, no build-time runtime, no dependencies. That was a deliberate constraint rather than a preference: - **It runs in someone else's page.** The collector is embedded in thousands of sites we do not control, alongside whatever else those sites load. It cannot assume a module system, a polyfill set, or a particular browser baseline, and it must not collide with anything already on the page. - **It has to be small and fast.** Anything shipped to every visitor of every customer is measured against the customer's own performance budget. A framework runtime would have been larger than the entire collector. - **It operates in a hostile environment.** The subjects of the measurement actively try to defeat it. Signal collection has to be defensive about a page that may be instrumented, patched, or emulated around it — and that argues for code with no layers between it and the browser APIs it reads. The collector's job ends at gathering and shipping signals. All judgment happens server-side, where the logic is not visible to the thing being judged. ### Go on the Hot Path Every service on the request path is written in **[Go](https://nerdy.pro/technologies/go)**. The workload is close to the language's ideal shape: enormous numbers of small, independent, mostly I/O-bound requests that have to be handled concurrently at predictable latency. Two properties mattered most in practice. Goroutines made per-request concurrency cheap enough that ingest, enrichment, and scoring could each fan out internally without a thread pool to tune. And Go's runtime gave us the tail-latency behaviour the product was sold on — the number that matters in a real-time system is not the average response but the slowest few percent, and that is exactly where a heavier runtime spends its budget. The path itself is deliberately short: ingest and validate, resolve context, evaluate the detection rules and models against the incoming signals, return the verdict. Anything that does not have to happen before the response — persistence, aggregation, enrichment, model feedback — is pushed off the hot path and onto the asynchronous pipeline behind it. ### From a Single Database to a Data Platform The platform launched on **[PostgreSQL](https://nerdy.pro/technologies/postgres)**, and that was the right call at the start. One database held the events, the accounts, the configuration, and the aggregates; one query language answered every question; the whole product could be reasoned about in one place while it was still finding its shape. It did not survive contact with production volume, and the reason is worth stating precisely: the failure was not "PostgreSQL is too slow", it was that a single store was being asked to be four different things at once. A high-rate append-only event sink, a low-latency lookup store on the request path, an analytical warehouse for aggregates over billions of rows, and a search index for investigating individual sessions are four workloads with four contradictory sets of requirements. Tuning for any one of them made the others worse. So we split them apart, and moved the platform onto **[Google Cloud](https://nerdy.pro/technologies/gcp)**: **Redis** — the hot path's state. [Redis](https://nerdy.pro/technologies/redis) holds everything a scoring decision needs to consult in single-digit milliseconds: recent per-visitor and per-session state, rate and frequency counters, resolved configuration, and reputation lookups. It is what makes an in-request verdict possible at all, and it absorbs the read volume that would otherwise have landed on the primary database. **Pub/Sub** — the seam between synchronous and asynchronous. Every scored request is published as an event and the response returns immediately; everything downstream consumes from there. This is the decoupling that lets the request path stay short and lets the rest of the platform be scaled, redeployed, or temporarily slowed without the scoring endpoint noticing. It also means a traffic spike lands in a queue rather than in a database. **Object storage** — raw events, kept indefinitely. Every signal payload is archived to S3-compatible object storage in its original form. It is the cheapest durable place to put data whose future use is not yet known, and it is the reason detection logic can be re-run retrospectively over real historical traffic instead of over a summary of it. **BigQuery** — the analytical layer. Aggregations across the whole event history, traffic-quality reporting, cohort and campaign analysis, and the exploratory work behind new detection heuristics all run here, on a warehouse built for scans of that size, and entirely off the operational path. **Elasticsearch** — investigation. When a customer disputes a verdict or an analyst chases a new pattern, the question is "show me these particular sessions, filtered six ways" — an interactive search problem rather than an aggregation one. Elasticsearch serves that, so exploratory queries never touch the systems that serve traffic. **PostgreSQL** stayed, with a much narrower remit: accounts, customer configuration, and the relational data that genuinely wants transactions and constraints. Removing the workloads it was never suited for is what made it a good fit again. ### Kubernetes on Google Cloud The platform runs as containerised services on a **[Kubernetes](https://nerdy.pro/technologies/kubernetes)** cluster in Google Cloud. Given the traffic profile, the alternative was never seriously on the table: load that swings by an order of magnitude within minutes needs infrastructure that adds capacity by itself, and services that are individually cheap to replicate. The split into small Go services pays off here. Ingest, scoring, and the asynchronous consumers have very different scaling curves, and as separate deployments each one scales on its own signal — the ingest tier follows request rate, the consumers follow queue depth. Rolling deployments meant the scoring endpoint could be updated during business hours without a maintenance window, which for a service sitting in a customer's page-load path was a hard requirement rather than a convenience. ### Results The client ended the engagement with a production platform that scores live traffic in real time, sustains tens of thousands of requests per minute with headroom for spikes several times that, and retains every raw signal it has ever seen — so a detection improvement made today can be validated against years of real traffic rather than against a synthetic benchmark. Just as importantly, the architecture separates concerns along the lines that actually matter to the business: the hot path stays small, fast, and boring, while analysis, investigation, and model work happen behind a queue where they can grow arbitrarily without ever threatening the response time a customer sees. This is the kind of system we take on end to end: high-throughput [Go](https://nerdy.pro/technologies/go) services, an event-driven data platform, and the [Kubernetes](https://nerdy.pro/technologies/kubernetes) infrastructure to run both. The collector itself — dependency-free JavaScript running in thousands of pages we do not control — is the discipline our [embeddable widgets & scripts](https://nerdy.pro/services/web-development/embeddable-widgets) page describes. If you have a workload shaped like this one, [tell us about it](https://nerdy.pro/contact). ## YouMi https://nerdy.pro/portfolio/youmi ### Project Overview YouMi is an online psychological counseling platform that connects users with certified psychologists through a mobile application. Users can pick a specialist, schedule and reschedule sessions, and chat with their psychologist in real time. When YouMi approached us, their Flutter application had serious technical problems. Users reported frequent disconnections during sessions, chat messages that failed to deliver, and navigation bugs that made the app frustrating to use. The problems were eroding user trust and holding back the platform's growth. ![Specialist Selection Screen](https://nerdy.pro/projects/youmi/images/01.webp) ![Session Scheduling](https://nerdy.pro/projects/youmi/images/02.webp) ![Session Features](https://nerdy.pro/projects/youmi/images/03.webp) ![User Profile](https://nerdy.pro/projects/youmi/images/04.webp) ![Preferences](https://nerdy.pro/projects/youmi/images/05.webp) ![Notification Settings](https://nerdy.pro/projects/youmi/images/06.webp) ### Technical Challenges We found two problems at the core of the application's instability: **WebSocket Connection Management**: The real-time chat and session features relied on WebSocket connections that were improperly handled. Connections would drop without proper reconnection logic, messages were lost during network transitions, and the app struggled to maintain stable communication between users and psychologists. This made the chat unreliable and disrupted active counseling sessions. **Navigation Architecture**: The application's routing system was poorly implemented, causing memory leaks, inconsistent state management, and unpredictable navigation behavior. Users experienced crashes when moving between screens, back button malfunctions, and screens that would freeze or display incorrect content. ### Solution & Implementation Working within a tight two-week timeframe, we systematically addressed both core issues: We refactored the WebSocket implementation around proper connection lifecycle management: automatic reconnection, message queuing during network interruptions, and real error handling. Chat messages now arrive reliably and sessions stay stable through network transitions and short connectivity drops. We also added message state tracking, so users and psychologists see when a message is sent, delivered, and read. We rebuilt the navigation system on Flutter's current routing practices: proper state management, the memory leaks cleaned up, and a predictable navigation flow. Screens stopped freezing, the back button started behaving, and the interface became consistent across the app. ### Results Within two weeks the application went from unstable to dependable. Chat became reliable, sessions stopped dropping, and the navigation problems were gone. YouMi could get back to growing its user base instead of managing technical crises. YouMi came to us as a [Flutter app development](https://nerdy.pro/services/flutter-app-development) rescue — see how we build and stabilize production Flutter apps, or read what we have learned about [healthcare and telemedicine apps](https://nerdy.pro/services/flutter-app-development/healthcare) since. ## dxpdf https://nerdy.pro/open-source/dxpdf ### The Problem Converting Word documents to PDF is one of the most common tasks in business software. Invoices, contracts, reports, compliance forms — they start as `.docx` files and need to become PDFs for sharing, archiving, or printing. The existing solutions all come with significant tradeoffs: - **Microsoft Office / LibreOffice** — requires installing a full office suite on every server. LibreOffice's headless mode is slow, memory-hungry, and produces inconsistent results across versions. Scaling means running multiple instances that consume gigabytes of RAM. - **Cloud APIs** (Google Docs, Adobe, CloudConvert) — adds latency, costs per conversion, and sends potentially sensitive documents to third-party servers. Not viable for regulated industries or air-gapped environments. - **HTML-to-PDF tools** (wkhtmltopdf, Puppeteer) — requires converting DOCX to HTML first, losing formatting fidelity. Tables, headers/footers, and page breaks rarely survive the round trip. None of these work well when you need fast, accurate, offline conversion at scale — especially in automated pipelines, CI/CD systems, or embedded applications where installing LibreOffice is not an option. ### How dxpdf Solves It dxpdf is a standalone DOCX-to-PDF converter written in Rust and powered by Google's Skia graphics library. It reads `.docx` files directly, parses the OOXML structure, and renders pixel-accurate PDF output — all in a single binary with no external dependencies beyond Skia. A Flutter-inspired **measure-then-position** model ensures that text wrapping, table sizing, and page breaks match what Microsoft Word produces: ```text DOCX (ZIP) → Parse → Document Model → Resolve → Layout → Subset → Paint → PDF Twips/Emu/HalfPoints ←──── Pt throughout ────→ Skia ``` Type-safe dimensions flow through the whole pipeline: OOXML units (`Twips`, `Emu`, `HalfPoints`) are `i64`-backed in the parsed model so they round-trip losslessly, layout works in typographic points (`Pt`), and raw `f32` appears only at the Skia rendering boundary — so mixing units is a compile error rather than a document that is subtly wrong. The result is a converter that turns a 3-page document with tables and images into a PDF in **170 ms** using **54 MB of memory**, and a 171-page, 14 MB document in **420 ms** — fast enough to run inline in a request handler or batch-process thousands of documents. ### What It Supports Validated against ISO 29500 (Office Open XML): **75 entries fully implemented, 12 partial, 11 not yet supported** — the [full matrix](https://github.com/nerdy-pro/dxpdf#ooxml-feature-coverage) lists every entry with its status. Highlights: - **Text formatting** — bold, italic, underline, highlighting, font size, font family, color, character spacing, character scaling, superscript, subscript, run shading, run borders - **Paragraphs** — alignment (left, center, right, justify, distribute), spacing, indentation, tab stops (incl. decimal, bar, and absolute-position), borders, shading, keep-with-next, keep-lines, widow/orphan control - **Tables** — column widths, cell margins with 3-level cascade, merged cells, row heights, borders, cell shading, table styles with conditional formatting, nested and floating tables, row splitting across pages - **Images** — inline (PNG, JPEG, GIF, BMP, WebP) and floating/anchored with alignment, wrapping, cropping, and percentage-based positioning - **Shapes and text boxes** — DrawingML and VML shapes, shape text bodies with insets, anchoring, autofit, custom geometry - **Styles** — paragraph and character styles with `basedOn` inheritance, document defaults, theme fonts - **Headers and footers** — text, images, page numbers (PAGE/NUMPAGES field codes), first-page and even/odd variants - **Lists** — multi-level numbering: bullets, decimal, letter, roman, ordinal and spelled-out text, plus non-Latin sequences and picture bullets - **Navigation** — clickable link annotations, bookmarks and cross-references as named destinations, and a PDF outline built from heading levels - **Sections** — multiple page sizes and margins, section breaks, multi-column layouts, portrait and landscape orientations - **Equations** — inline OMML (`m:oMath`) math: math runs, superscripts and fractions, set in Word's default math face and able to carry a hyperlink or appear in an outline title - **Layout** — automatic pagination, footnotes and endnotes, word wrapping, line spacing modes, floating image text flow - **Internationalization** — UAX #14 line breaking (incl. Thai, Lao, Khmer, Burmese), UAX #9 bidirectional text with mirroring, HarfBuzz shaping for cursive-joining scripts, and `w:lang`-driven decimal separators, date pictures, and spelled-out numbers - **Text and emoji** — grapheme-correct segmentation and full-color emoji including ZWJ, modifier, keycap and flag sequences ### Four Ways to Use It #### Command-line tool Install and run with a single command: ```shell cargo install dxpdf dxpdf input.docx -o output.pdf ``` #### Rust library One function call — bytes in, bytes out: ```rust let docx_bytes = std::fs::read("document.docx")?; let pdf_bytes = dxpdf::convert(&docx_bytes)?; std::fs::write("output.pdf", &pdf_bytes)?; ``` For more control, inspect the parsed document model before rendering: ```rust use dxpdf::{docx, model, render}; let document = docx::parse(&std::fs::read("document.docx")?)?; for block in &document.body { match block { model::Block::Paragraph(p) => { /* inspect paragraph */ } model::Block::Table(t) => { /* inspect table */ } model::Block::SectionBreak(props) => { /* inspect section properties */ } } } let pdf_bytes = render::render(document, &dxpdf::RenderOptions::default())?; ``` #### Python package Install from PyPI and use in any Python application: ```shell pip install dxpdf ``` ```python import dxpdf # Bytes in, bytes out pdf_bytes = dxpdf.convert(open("input.docx", "rb").read()) # File to file dxpdf.convert_file("input.docx", "output.pdf") ``` Since 0.8.1 both calls release the GIL while the Rust engine works, so a thread pool converting several documents at once actually runs them in parallel rather than serializing on the interpreter. #### Go package Install with `go get` and call it like any other package: ```shell go get github.com/nerdy-pro/dxpdf/go ``` ```go import "github.com/nerdy-pro/dxpdf/go" // Bytes in, bytes out pdfBytes, err := dxpdf.Convert(docxBytes) // File to file err := dxpdf.ConvertFile("input.docx", "output.pdf") // Override the embedded-image resolution (default 220 DPI) pdfBytes, err := dxpdf.ConvertWithOptions(docxBytes, 300) ``` The Go package is a thin cgo layer over the same Rust engine the CLI and the Python package use, so it needs `CGO_ENABLED=1` and a C compiler. The prebuilt library for every supported platform is committed in the repository, so there is no separate fetch or build step. It runs on linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 and — since 0.8.1 — windows/amd64. Windows is the one platform that deploys differently: the other four link a static archive that is absorbed into your binary, while Windows loads `dxpdf.dll` dynamically, so the built executable needs that DLL beside it or on `PATH` at runtime. Copy it out of the module cache into your release output. The module also carries no tags of its own, so `go get .../go@v0.8.1` will not resolve; plain `go get`, `@main`, or a commit SHA to pin does. ### Performance Benchmarked on Apple M3 Max with [hyperfine](https://github.com/sharkdp/hyperfine) (30 runs, 5 warmup) at v0.5.1, against fixtures committed in the repository. Times are rounded to 5 ms, and run-to-run spread on a normally loaded machine is around ±10 ms, so smaller differences are not meaningful: | Fixture | Pages | Input | Conversion time | Peak RSS | | --- | --- | --- | --- | --- | | 3-page business document | 3 | 34 KB | **170 ms** | 54 MB | | 7-page document | 7 | 10 KB | **170 ms** | 51 MB | | 9-page, image-heavy document | 9 | 1.3 MB | **55 ms** | 40 MB | | 171-page report | 171 | 14 MB | **420 ms** | 145 MB | **Font resolution, not document size, decides what a conversion costs.** The 9-page fixture carries forty times the input of the 3-page one and converts in a third of the time, because its fonts are embedded or already present on the host — that path costs about 4 ms, where falling back to the host metadata index costs 120–185 ms, once. For a batch workload the useful question is not how large the documents are but whether they name fonts the host has. Conversion correctness is locked in by fixture-driven tests, including visual regression tests that compare rendered PDFs against Word-generated reference documents. ### Real-World Use Cases #### Automated document pipelines CI/CD systems or batch processors that generate contracts, invoices, or reports from `.docx` templates. dxpdf runs as a single binary — no LibreOffice installation, no Docker image with a full desktop environment, no per-document API costs. #### Regulated environments Healthcare, legal, and financial applications where documents cannot leave the network. dxpdf runs fully offline with no external service calls, so it works in air-gapped and on-premises deployments. #### Embedded and edge computing IoT devices, kiosks, or lightweight containers where installing a 500 MB office suite is not practical. dxpdf's tens-of-megabytes memory footprint and sub-second conversion times make it viable for resource-constrained environments. #### Python and Go backends Django, Flask, or FastAPI services that need to convert uploaded DOCX files on the fly. The Python bindings wrap the Rust core via PyO3, so you get native performance without a subprocess or external service. Go services get the same engine through cgo: a handler calls `dxpdf.Convert` directly instead of shelling out to a binary or calling a sidecar, which keeps conversion inside the request's own goroutine and error handling. ### Release Notes The latest release is **0.8.1**. Four releases have landed since 0.5.0, and between them they added a language binding and a new class of content: - **0.6.0** — Go bindings over a new C ABI, plus table fixes: the outer border of a spaced table, `vMerge` overflow going to a span's last row, vertical `inside`/`outside` borders mirroring with page parity, and `w:vanish` hidden runs being removed from layout instead of painted. - **0.7.0** — OMML equation rendering, alongside fixes for continuous-break column overlap, UAX #14 LB25 token wrapping, and the Symbol PUA table. Equations carry hyperlinks and appear in outline titles. - **0.8.0** — the math path refined: fraction geometry made consistent, superscripts over fraction bases, footnote number prefixes taking the opening equation's font, and hyphen handling fixed for non-ASCII digits. It also reads §22.9.2.15 universal measures and §22.9.2.9 percent spellings. - **0.8.1** — windows/amd64 joins the Go bindings, and the Python bindings release the GIL for the duration of a conversion. The long-form write-up of 0.5.0, [dxpdf 0.5.0: Teaching a DOCX Converter to Read the Rest of the World](https://nerdy.pro/blog/dxpdf-0-5-0-release), still covers the internationalization work that underpins all of this — UAX #14 line breaking, UAX #9 bidirectional text, and CLDR-driven numbers and dates that follow the document's own language. #### Does dxpdf require Microsoft Office or LibreOffice? No. dxpdf is a standalone converter that reads DOCX files directly and renders PDFs using Google's Skia graphics engine. It has no dependency on any office suite. #### What languages and platforms does dxpdf support? dxpdf is written in Rust and available as a CLI tool (via cargo install), a Rust library (via crates.io), a Python package (via PyPI), and Go bindings (via go get github.com/nerdy-pro/dxpdf/go). All four run on macOS, Linux and Windows; the Go bindings cover linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 and, since 0.8.1, windows/amd64. #### How accurate is the conversion? dxpdf uses a Flutter-inspired measure-then-position pipeline designed for pixel-level fidelity, validated against ISO 29500 with 75 entries fully implemented, 12 partial and 11 not yet supported. Visual regression tests compare output against Word-generated references. #### How fast is dxpdf? On Apple M3 Max, dxpdf converts the committed fixtures in 55-170 ms and a 171-page, 14 MB document in about 420 ms. Font resolution matters more than document size: fonts that are embedded or already on the host resolve in about 4 ms, while falling back to the host metadata index costs 120-185 ms once. It is fast enough to run inline in web request handlers. #### Can I use dxpdf in a Python application? Yes. Install with pip install dxpdf. The Python package wraps the Rust core via PyO3, providing native performance. Use dxpdf.convert() for bytes-in/bytes-out or dxpdf.convert_file() for file-to-file conversion. #### Can I use dxpdf from Go? Yes, since 0.6.0. Run go get github.com/nerdy-pro/dxpdf/go and call dxpdf.Convert for bytes-in/bytes-out or dxpdf.ConvertFile for file-to-file; ConvertWithOptions and ConvertFileWithOptions take an image DPI. It is a cgo layer over the same Rust engine, so it needs CGO_ENABLED=1 and a C compiler. It supports linux and macOS on amd64 and arm64, and since 0.8.1 windows/amd64 as well — on Windows the binding loads dxpdf.dll dynamically, so ship that DLL alongside your executable. The Go module has no tags of its own, so pin with a commit SHA rather than a version tag. #### What DOCX features are not yet supported? Not yet supported: Indic script reordering, automatic hyphenation, tracked changes and comments, SmartArt and charts, page borders and the document grid, WMF and SVG images, shadow, outline, emboss and imprint text effects, pattern cell shading, mirrored tab stops and numbering labels under w:bidi, and the counting-system numbering formats such as chineseCounting. Partially supported: strikethrough and small caps are parsed but not rendered, most border styles are approximated as solid lines, tight and through image wrapping use the bounding box rather than a polygon, EMF images decode only a single embedded bitmap, even, odd and nextColumn section breaks are treated as nextPage, percentage and auto cell widths fall back to the table grid, and per-glyph font fallback draws a missing codepoint from whichever host face covers it, so output is host-dependent and no w:lang hint is passed yet, which can give Han text the wrong language's glyph shapes. ## Flutter Adaptive Layout https://nerdy.pro/open-source/flutter-adaptive-layout ### The Problem A Flutter app that ships to phones, tablets, and desktop needs more than a flexible column. A screen that reads well at 390 logical pixels wide wants a centred card at 700 and a sidebar at 1200. Flutter gives you the measurements but no structure for acting on them. So the checks spread out. A `MediaQuery.of(context).size.width > 600` here, a `LayoutBuilder` there, a ternary inside a `Padding` three widgets down. Each one is defensible on its own; together they are a layout policy that is written down in no single place. Common issues include: - Breakpoint values duplicated across dozens of widgets, drifting apart as the design changes - `LayoutBuilder` reporting the parent's constraints when what you wanted was the device - Layouts that flip to a tablet variant the moment a phone is rotated to landscape - Widget trees where the responsive branching is impossible to see without reading every child ### How flutter_adaptive_layout Solves It `AdaptiveLayout` takes one builder per screen size and a `child`. It measures the device through `MediaQuery`, maps that to a `ScreenSize`, and builds only the matching variant — with the `child` created once and handed to whichever builder wins. ```dart AdaptiveLayout( smallBuilder: (context, child) => child!, mediumBuilder: (context, child) => Center(child: child), largeBuilder: (context, child) => Row( children: [const Sidebar(), Expanded(child: child!)], ), child: const MyHomePage(), ) ``` The responsive decision now lives in one widget, at the top of the subtree it governs, written as three layouts rather than a chain of conditions. ![The example app's small-screen layout, rendered on an iPhone 14](https://nerdy.pro/open-source/flutter-adaptive-layout/iphone_14.png) ![The same app on an iPad Pro 12.9-inch, where largeBuilder puts the content in a wider card](https://nerdy.pro/open-source/flutter-adaptive-layout/ipad_12_inch.png) ### What It Supports - **Screen-size-driven layouts** — `smallBuilder`, `mediumBuilder`, and `largeBuilder`, each optional; a missing one falls back to `child`, so you can style only the sizes you care about - **Breakpoints you control** — defaults of 400 and 600 logical pixels, overridable per widget or for the whole app - **Build the child once** — switching layouts wraps your content instead of rebuilding it from scratch - **Rotation stability** — classification reads `MediaQuery.of(context).size.shortestSide`, so a phone stays small in landscape - **Pluggable sizing logic** — implement `ScreenSizeQualifier` to classify by width, platform, hinge state, or whatever your design system uses - **No dependencies** — pure Flutter on top of `MediaQuery`, working on iOS, Android, web, macOS, Windows, and Linux ### How Screen Size Is Decided The default `BreakpointsQualifier` compares the shortest side against two breakpoints, inclusively — a value exactly equal to a breakpoint belongs to the smaller bucket. | Screen size | Condition | Default range | Typical device | | --- | --- | --- | --- | | `ScreenSize.small` | `shortestSide <= smallBreakpoint` | 0–400 logical pixels | Phones | | `ScreenSize.medium` | `shortestSide <= mediumBreakpoint` | 401–600 logical pixels | Small tablets | | `ScreenSize.large` | above `mediumBreakpoint` | 601+ logical pixels | Large tablets, desktop | Breakpoints resolve in a fixed order, first defined value winning: the arguments passed to `BreakpointsQualifier`, then the nearest ancestor `BreakpointsSetting`, then the built-in 400 and 600. ### Real-World Use Cases #### Phone-first apps growing a tablet build An app that started on phones can keep every screen it already has and add a `largeBuilder` where a wider layout pays off. Screens with no builder for a size render the `child` unchanged, so the migration happens screen by screen instead of all at once. #### Master–detail navigation A list that pushes a detail route on a phone can render list and detail side by side on a tablet. The two variants are declared next to each other in one widget, which makes the difference between them reviewable in a diff. #### Design systems with their own breakpoints Wrap `MaterialApp` in `BreakpointsSetting` once and every `AdaptiveLayout` in the tree picks up the design system's numbers. Changing them later is a one-line edit, not a search across the codebase. #### Web and desktop windows On web and desktop the measured size is the window, so layouts respond as the user resizes it — the same code path that handles a tablet handles a resized browser. ### Getting Started Install the package: ```shell flutter pub add flutter_adaptive_layout ``` Requires Dart `>=2.18.5 <4.0.0` and Flutter 1.17+. Then import it and wrap the widget whose layout should adapt: ```dart import 'package:flutter_adaptive_layout/flutter_adaptive_layout.dart'; AdaptiveLayout( largeBuilder: (context, child) => Center( child: SizedBox(width: 600, child: child), ), child: const ArticleView(), // used as-is on small and medium screens ) ``` To classify screens differently, implement `ScreenSizeQualifier` and pass it as `qualifier`: ```dart class WidthQualifier extends ScreenSizeQualifier { @override ScreenSize qualify(BuildContext context) { final width = MediaQuery.of(context).size.width; if (width < 600) return ScreenSize.small; if (width < 1024) return ScreenSize.medium; return ScreenSize.large; } } ``` `qualify` runs on every build, so keep it cheap and free of side effects. A complete, runnable app is in the [example directory](https://github.com/nerdy-pro/flutter-adaptive-layout/tree/main/example). #### How is this different from LayoutBuilder? LayoutBuilder reports the constraints handed down by the parent widget, which may be far smaller than the display. AdaptiveLayout classifies the device or window through MediaQuery, so a widget nested deep in the tree still knows it is running on a tablet. Use LayoutBuilder to fit an available box; use AdaptiveLayout to choose a layout for the device. #### Does the layout change when the device rotates? No. Classification uses MediaQuery.of(context).size.shortestSide, which is the same in portrait and landscape, so a phone stays ScreenSize.small when rotated. If you want orientation-sensitive layouts, implement a custom ScreenSizeQualifier that reads size.width instead. #### Do I have to provide all three builders? No. Any builder can be omitted and the child is used for that screen size instead. Providing neither a matching builder nor a child throws an UnimplementedError. #### How do I change the default breakpoints? Either pass BreakpointsQualifier(smallBreakpoint: ..., mediumBreakpoint: ...) to a single AdaptiveLayout, or wrap your app in BreakpointsSetting to change them everywhere. Constructor values win over BreakpointsSetting, which wins over the defaults of 400 and 600. #### Does it work on web and desktop? Yes. It depends only on MediaQuery, so it runs on every Flutter target. On web and desktop the measured size is the window, so layouts respond as the window is resized. ## Flutter Future Progress Dialog https://nerdy.pro/open-source/flutter-progress-dialog ### The Problem Most Flutter apps need to run tasks that take a few seconds — fetching data from an API, uploading a file, or processing a payment. During that wait the user stares at a frozen screen with no indication that anything is happening. They tap the button again, trigger duplicate requests, or assume the app has crashed. The standard solution is to manually manage a loading state: flip a boolean, show a spinner, await the future, hide the spinner, then handle the result or error. That logic ends up duplicated across dozens of screens with subtle differences each time. Common pain points include: - Boilerplate loading-state management repeated in every screen - Users tapping buttons multiple times because there is no visual feedback - Dismissed dialogs that leave orphaned futures still running in the background - Inconsistent error handling — some screens catch exceptions, others crash silently ### How flutter_future_progress_dialog Solves It `flutter_future_progress_dialog` replaces all of that boilerplate with a single function call. Pass your async task in, and the package shows a non-dismissible progress dialog, waits for the result, closes the dialog, and hands you back a type-safe result — either `Success` with the value or `Failure` with the error. ![Flutter progress dialog demo on iPhone](https://nerdy.pro/open-source/flutter-progress-dialog/flutter_progress_dialog.gif) Error handling is built into the result type, so you never forget to catch an exception. ### What It Supports - **Material dialog** — `showProgressDialog` displays a standard Material circular progress indicator - **Cupertino dialog** — `showCupertinoProgressDialog` shows an iOS-styled activity indicator - **Adaptive dialog** — `showAdaptiveProgressDialog` picks the right style for the current platform automatically - **Custom dialog UI** — pass a `builder` to replace the default indicator with any widget you need - **Type-safe results** — `ProgressDialogResult` is a sealed class with `Success` and `Failure` variants, supporting pattern matching and convenience methods like `unwrap()` and `map()` - **Error capture** — `Failure` includes both the error object and the stack trace ### Real-World Use Cases #### Payment processing A checkout screen calls a payment gateway API. Without a progress dialog, users tap "Pay" again when nothing seems to happen — causing duplicate charges. With `showProgressDialog`, the dialog stays on screen and blocks interaction until the gateway responds, and your code pattern-matches the result to navigate to a confirmation or error screen. #### File uploads A document scanner uploads images to a server. The upload can take several seconds on slow connections. Wrapping the upload in `showAdaptiveProgressDialog` gives users immediate visual feedback and ensures the dialog matches the platform — Material on Android, Cupertino on iOS. #### Form submissions A multi-step form sends data to a backend on the final step. Using the progress dialog prevents double-submission and gives you a clean `Success`/`Failure` result to decide whether to show a success screen or an inline error message. #### Background data sync A CRM app syncs local changes with a remote server. A custom builder can show a branded loading animation instead of the default spinner, keeping the user experience consistent with the rest of the app. ### Getting Started Install the package: ```shell flutter pub add flutter_future_progress_dialog ``` Then call `showProgressDialog` with your async task: ```dart final result = await showProgressDialog( context: context, future: () => fetchData(), ); switch (result) { case Success(:final value): // Use the value case Failure(:final error): // Handle the error } ``` Three dialog functions are available: `showProgressDialog` (Material), `showCupertinoProgressDialog` (Cupertino), and `showAdaptiveProgressDialog` (platform-automatic). All accept an optional `builder` parameter for custom UI. A complete working example is available in the [example directory](https://github.com/nerdy-pro/flutter-progress-dialog/tree/main/example). #### Can the user dismiss the dialog manually? No. The dialog is non-dismissible by default, which prevents orphaned futures and duplicate submissions. The dialog closes automatically when the async task completes. #### What happens if the future throws an exception? The dialog closes and the result is returned as a Failure containing the error and stack trace. Your code can pattern-match on the result to handle errors without try-catch blocks. #### Does it work on both Android and iOS? Yes. Use showProgressDialog for Material style, showCupertinoProgressDialog for iOS style, or showAdaptiveProgressDialog to automatically match the host platform. #### Can I customize the loading indicator? Yes. Pass a builder parameter to any of the three dialog functions to replace the default progress indicator with your own widget — a branded animation, a message, or any other Flutter widget. #### How does the result type work? ProgressDialogResult is a sealed class with two variants: Success containing the returned value, and Failure containing the error and stack trace. You can use Dart pattern matching, or convenience methods like isSuccess, isError, unwrap(), and map(). ## Flutter Number Editing Controller https://nerdy.pro/open-source/flutter-number-editing-controller ### The Problem Flutter's built-in `TextEditingController` treats numbers as plain text. When a user types `1000000` into a price field, they see exactly that — an unreadable wall of digits. There is no grouping, no decimal formatting, and no currency symbol. Developers are left writing custom `TextInputFormatter` logic, manually parsing strings, and fighting cursor-position bugs every time formatting changes the text length. Common issues include: - Users misread large numbers without thousand separators - Currency fields that ignore locale conventions (symbol placement, comma vs. dot) - Fragile parsing code that breaks when the formatted text contains non-digit characters - Cursor jumping to unexpected positions after each keystroke ### How number_editing_controller Solves It `number_editing_controller` is a drop-in replacement for `TextEditingController`. Assign it to any `TextField` and it handles formatting, parsing, and cursor management automatically. The user sees properly formatted text like `$1,234.56` or `1.234,56 €`, while your code reads the clean numeric value through `controller.number`. ![Currency formatting demo](https://nerdy.pro/open-source/number-editing-controller/currency.gif) ### What It Supports - **Integer formatting** — groups digits with locale-appropriate separators (`1,000,000` in English, `1.000.000` in German) - **Decimal formatting** — configurable minimum and maximum fraction digits - **Currency formatting** — places the currency symbol before or after the number based on locale rules - **Locale awareness** — uses ICU formatting patterns so `1234.56` in USD renders as `$1,234.56` in English and `1.234,56 $` in German - **Negative numbers** — optional `allowNegative` flag to restrict input to positive values - **Runtime changes** — switch locale, currency, or precision on the fly without recreating the controller - **External currency symbol** — display the symbol as a `TextField` prefix/suffix decoration instead of inline text ### Real-World Use Cases #### E-commerce checkout A payment form needs to accept amounts in multiple currencies. With `number_editing_controller`, you create a single currency controller and change `currencyName` when the user picks a different currency. The field reformats instantly — no rebuild required. #### Multi-locale financial apps A banking app serving users in the US, Germany, and Japan needs each locale's conventions: `$1,234.56` vs `1.234,56 €` vs `¥1,500`. The controller handles all of this from a single `locale` parameter. ![Integer formatting demo](https://nerdy.pro/open-source/number-editing-controller/integer.gif) #### Inventory and quantity fields Warehouse apps deal with large integer quantities. The integer controller turns `1000000` into `1,000,000`, making data entry errors easy to spot. #### Analytics dashboards Decimal controllers with configurable precision let you format metric values — percentages with 2 digits, scientific measurements with 4 — while keeping the raw `num` value available for calculations. ### Locale Formatting Examples | Locale | Type | Raw Value | Displayed | | --- | --- | --- | --- | | `en` | Currency (USD) | `1234.56` | `$1,234.56` | | `de` | Currency (EUR) | `1234.56` | `1.234,56 €` | | `fr` | Currency (EUR) | `1234.56` | `1 234,56 €` | | `ja` | Currency (JPY) | `1500` | `¥1,500` | | `ru` | Currency (RUB) | `500` | `500 ₽` | | `en` | Integer | `1234567` | `1,234,567` | | `de` | Decimal (max 2) | `1234.89` | `1.234,89` | ### Getting Started Install the package: ```shell flutter pub add number_editing_controller ``` Requires Flutter 3.19+ and Dart 3.3+. Then create a controller and assign it to your `TextField`: ```dart final controller = NumberEditingTextController.currency( currencyName: 'USD', locale: 'en', ); TextField( controller: controller, keyboardType: TextInputType.numberWithOptions(decimal: true, signed: true), ) // Read the numeric value at any time final amount = controller.number; // e.g. 1234.56 ``` Three controller types are available: `.integer()`, `.decimal()`, and `.currency()`. All options — locale, separators, currency, precision — can be changed at runtime without recreating the controller. A complete working example with a currency picker, locale switcher, and external symbol placement is available in the [example directory](https://github.com/nerdy-pro/flutter_number_editing_controller/tree/main/example). #### Does it work with Flutter web? Yes, but JavaScript represents all numbers as 64-bit floats, so integers above 2^53 - 1 (about 9 quadrillion) lose precision silently. On native platforms, 64-bit integers are fully supported. #### Can I change the currency or locale after creating the controller? Yes. All formatting options are mutable at runtime. Changing locale, currency name, separators, or precision immediately reformats the displayed text without recreating the controller. #### Does it support Indian numbering (lakh/crore)? Not yet. The library uses uniform group sizes derived from ICU locale patterns (typically groups of 3). Variable-width grouping systems like Indian numbering are not supported. #### How do I display the currency symbol outside the text field? Set showCurrencySymbol to false and use the resolvedCurrencySymbol and currencySymbolPosition properties to place the symbol as a TextField prefix or suffix decoration. #### What happens with very long numbers? Decimal precision is capped at 20 fraction digits (Dart's toStringAsFixed limit). Extremely long inputs may degrade formatting performance, but typical financial and business values are handled without issues. ## Nerdy Pro Dev Container https://nerdy.pro/open-source/nerdy-pro-dev-container ### The Problem VS Code dev containers are the cleanest way to give every contributor on a project an identical environment — same OS, same tool versions, same shell. But that consistency comes at a cost: the first time anyone opens the project, VS Code has to build the container image from scratch. Installing an OS base layer, a language runtime, a shell, and any AI tooling on top of it can take several minutes, and it happens again on every cache-busting change to the `Dockerfile`. For teams that also rely on Claude Code inside the container, there's a second layer of friction: getting authentication, shell history, and npm caches to survive a container rebuild requires the same boilerplate `devcontainer.json` in every repository. ### How It Solves It `nerdy-pro-dev-container` is a prebuilt, multi-architecture Docker image published to GitHub Container Registry. Instead of building a dev container image per project, a repository points its `devcontainer.json` at the published image and VS Code pulls it directly — no build step, no waiting. The image bundles everything a Node.js-based project needs to start working immediately: - **Ubuntu 26.04** base (Microsoft's official `devcontainers/base` image) - **Node.js LTS**, installed via NodeSource - **Claude Code**, installed globally via npm - **zsh with oh-my-zsh**, configured with the `git` and `fzf` plugins as the default login shell - **Common CLI tools** — git, curl, wget, jq, gpg, OpenSSH client, fzf - **Passwordless sudo** for the `vscode` user It's built for both `linux/amd64` and `linux/arm64`, so the same tag works on Intel/AMD machines and Apple Silicon. ![VS Code open in the Nerdy Dev Container, with Claude Code running in the integrated terminal and side panel](https://nerdy.pro/open-source/nerdy-pro-dev-container/screenshot.webp) ### Persistence Across Rebuilds A container that gets rebuilt on every `Dockerfile` change is only convenient if state doesn't reset with it. The template wires up four named Docker volumes so nothing is lost between rebuilds: - Claude Code configuration and session data (`.claude` directory, via `CLAUDE_CONFIG_DIR`) - zsh command history (`HISTFILE` on a persistent volume) - npm package cache - SSH `known_hosts`, so git host trust doesn't need re-establishing every time Git authentication forwards the host's SSH agent into the container, and the host's `gitconfig` is propagated automatically — commits made inside the container carry the right author identity without any manual setup. ### Getting Started Copy the template into `.devcontainer/devcontainer.json`: ```json { "name": "my-project", "image": "ghcr.io/nerdy-pro/nerdy-pro-dev-container:latest" } ``` Reopen the project in a dev container, and Node.js, zsh, and Claude Code are ready with no build step. #### Authenticating Claude Code Claude Code inside the container reads a `CLAUDE_CODE_OAUTH_TOKEN` environment variable rather than going through an interactive login. Generate a long-lived token on the host with: ```shell claude setup-token ``` Store it in the host's credential manager — Keychain on macOS, the desktop keyring (`secret-tool`) on Linux, or DPAPI on Windows — and forward it into the container with a `remoteEnv` entry in `devcontainer.json`, so it never has to be typed inside the container itself. #### First git operation The first push or fetch inside a fresh container prompts for host key verification in the terminal — a one-time confirmation that establishes SSH trust for that git host, after which it's cached in the persistent `known_hosts` volume. ### Versioning and Releases Images are built and published only from GitHub releases, not from every commit — a `Dockerfile` change on `main` doesn't affect anyone until a release is cut. Each release publishes four tags: | Tag | Behavior | | --- | --- | | `1.0.0` | Pinned — never changes | | `1.0` | Updates on patch releases | | `1` | Updates on minor and patch releases | | `latest` | Always the newest stable release | That range lets a project pin to an exact version for reproducibility, or track `latest` to stay current automatically. ### License The Dockerfile, the `devcontainer.json` template, and the documentation are MIT licensed. Everything installed on top of the base image — Ubuntu, Node.js, zsh, fzf, Claude Code — keeps its own original license. #### What is nerdy-pro-dev-container? It's a prebuilt, multi-architecture Docker image for VS Code Dev Containers that bundles Node.js LTS, zsh, and Claude Code, published to GitHub Container Registry as ghcr.io/nerdy-pro/nerdy-pro-dev-container. A project points its devcontainer.json at that image and skips the build step entirely. #### Do I need to build the image myself? No. The image is published to GitHub Container Registry and pulled directly by VS Code. Pointing devcontainer.json at ghcr.io/nerdy-pro/nerdy-pro-dev-container:latest skips the build step entirely. #### What's preinstalled in the container? Ubuntu 26.04, Node.js LTS, Claude Code, zsh with oh-my-zsh (git and fzf plugins), and common CLI tools including git, curl, wget, jq, gpg, and an OpenSSH client. #### How does Claude Code get authenticated inside the container? Run claude setup-token on the host to generate a long-lived token, store it in the host's credential manager, and forward it into the container as the CLAUDE_CODE_OAUTH_TOKEN environment variable via devcontainer.json's remoteEnv. #### Does state survive a container rebuild? Yes. Four Docker volumes persist Claude Code configuration, zsh history, the npm cache, and SSH known_hosts across rebuilds, so re-authentication and re-trust aren't needed every time. #### Which architectures are supported? The image is built for both linux/amd64 and linux/arm64, so the same tag runs on Intel/AMD machines and Apple Silicon. #### How does versioning work? Images are published only from GitHub releases. Each release creates four tags — an exact version like 1.0.0, a minor version like 1.0, a major version like 1, and latest — so a project can pin to a specific version or track the newest stable release. ## Orosu Server https://nerdy.pro/open-source/orosu-server > From Japanese 降ろす (*órosu*) — "to unload" or "offload." ### The Problem CI systems are good at building software and bad at delivering it. Getting a build artifact from a pipeline onto a production machine usually means one of a few well-worn but fragile patterns: - **SSH private keys** copied into CI secrets and spread across every pipeline and repository that needs to deploy - **SFTP/rsync scripts** copy-pasted from one project to the next, each with its own slightly different bugs - **Hardcoded paths and permissions** that break the moment a server's layout changes - **Production machines directly reachable** from CI runners, widening the attack surface every pipeline adds to - **Secret sprawl** that turns rotating a single compromised key into an audit of every repository that might reference it None of this is specific to any one CI/CD platform — it's the default shape of "run a script on a remote machine from a pipeline," and it accumulates risk with every project that adopts it. ### How Orosu Solves It Orosu installs once on the target machine as `orosu-server` and turns deployment into a bounded, authenticated request instead of an open SSH session: 1. **CI builds** the application — a binary, a container image, static assets, whatever the pipeline produces 2. **CI triggers a job** over a WebSocket connection, optionally attaching files 3. **`orosu-server` authenticates** the request using Ed25519 signatures — no passwords, no shared secrets in transit 4. **`orosu-server` runs a predefined script** — one of a fixed set configured on the server, never arbitrary CI-supplied commands 5. **The script deploys**, using whatever files and arguments the job attached No direct SSH, no per-pipeline credential juggling, and no server-side surface beyond the specific scripts an operator has explicitly configured. ### Installation `orosu-server` and its companion `orosu-keygen` CLI ship as Debian/Ubuntu packages from an apt repository: ```shell curl -fsSL https://packages.nerdy.pro/NerdyPro.gpg | sudo gpg --dearmor -o /usr/share/keyrings/nerdy-pro.gpg echo "deb [signed-by=/usr/share/keyrings/nerdy-pro.gpg] https://packages.nerdy.pro/ stable main" | sudo tee /etc/apt/sources.list.d/nerdy-pro.list sudo apt update sudo apt install orosu ``` GitHub Releases also publish prebuilt `orosu-server` and `orosu-keygen` binaries directly, for machines that can't add an apt source. ### Getting Started **Generate a client key pair** with `orosu-keygen`, from `/etc/orosu`: ```shell orosu-keygen --name my-ci-client --private-key-output my-ci-client.key --public-key-output my-ci-client.pub ``` The public key goes into the server config; the private key becomes a CI secret. **Configure the server** in `/etc/orosu/orosu-server.toml` — a listen address and the client's public key: ```yaml listen: tcp: "127.0.0.1:8081" clients: - name: my-ci-client secret_file: /etc/orosu/my-ci-client.pub ``` A server bound to localhost is typically fronted by a reverse proxy that terminates TLS and forwards WebSocket upgrades — the README documents an nginx configuration for this. **Define a script** the client is allowed to trigger: ```bash #!/bin/bash echo "Hello, $1!" ``` ```yaml clients: - name: my-ci-client secret_file: /etc/orosu/my-ci-client.pub scripts: - name: test-script command: - "bash" - "/etc/orosu/scripts/test.sh" ``` **Trigger it from CI** with the companion GitHub Action, passing the server address and the client's private key as secrets: ```yaml - name: Remotely execute a script uses: orosu-ci/orosu@v0 with: address: ${{ secrets.OROSU_SERVER_URL }} script: test-script key: ${{ secrets.OROSU_CLIENT_KEY }} arguments: "from CI pipeline" ``` ### End-to-End Encryption WSS/:termTLS covers the connection, but when TLS is terminated at a reverse proxy in front of `orosu-server` — the standard setup above — script arguments, uploaded files, and streamed output are plaintext at that proxy. An optional end-to-end encryption handshake (X25519 + HKDF-SHA256 + ChaCha20-Poly1305) closes that gap: ```shell orosu-keygen --kind server --private-key-output server.key --public-key-output server.pub ``` The server's public key is added to its config and handed to CI as the Action's `server_key` input. The upgrade is opt-in and additive on both ends independently — a server with encryption configured still serves clients that omit `server_key` exactly as before, and no coordinated rollout is required. ### Security Security constraints shaped the design: - **Cryptographic authentication** — every job is signed with the client's Ed25519 key; there's no password or shared secret on the wire for an attacker to intercept. - **A closed set of actions** — CI can only trigger scripts an operator has explicitly predefined on the server. There's no path from a CI job to an arbitrary shell command. - **Hardened attachment handling** — zip entry names are validated against path traversal, and entry count and decompressed size are both capped, so a crafted or oversized attachment can't escape its extraction directory or exhaust disk. - **Defense-in-depth encryption** — the optional end-to-end handshake checks for low-order-point attacks on its X25519 exchange, on top of the confidentiality it already provides. - **Secure-by-default file permissions** — `orosu-keygen` writes private key files with `0600` permissions regardless of the umask in effect. These protections landed in the 0.7.0 release as a drop-in upgrade — no config, CLI, or protocol changes required. See [`CHANGELOG.md`](https://github.com/orosu-ci/server/blob/main/CHANGELOG.md) for the full release history. ### License Orosu is licensed under Apache-2.0. #### What does orosu-server replace? Ad-hoc SSH/SCP deployment steps in CI pipelines — private keys in secrets, copy-pasted rsync scripts, and directly reachable production servers. CI triggers an authenticated WebSocket job instead of opening an SSH session. #### How does authentication work? Each CI client has an Ed25519 key pair. The public key is registered in the server's config; the private key is stored as a CI secret and used to sign job requests. The server only runs scripts it has explicitly configured for that client. #### Can CI run arbitrary commands on the server? No. orosu-server only executes predefined scripts configured in orosu-server.toml ahead of time. A CI job selects a script by name and passes arguments and attachments — it cannot supply arbitrary commands. #### Is the connection encrypted? WSS/TLS covers the connection by default, typically terminated at a reverse proxy in front of orosu-server. An optional end-to-end encryption layer (X25519 + ChaCha20-Poly1305) can be enabled so script arguments, files, and output stay confidential even from that proxy. #### How is orosu-server installed? Via an apt repository (packages.nerdy.pro) for Debian/Ubuntu, or as prebuilt binaries attached to GitHub Releases for orosu-server and the orosu-keygen CLI. #### What security measures does orosu-server include? Ed25519-signed requests, a closed set of server-defined scripts CI can trigger, path-traversal and size limits on attachment extraction, low-order-point protection on the optional end-to-end encryption handshake, and secure-by-default file permissions for generated keys. ## How to Choose a Flutter Development Company: The Checklist We Would Use on Ourselves https://nerdy.pro/blog/how-to-choose-a-flutter-development-company **TL;DR.** Every agency website says the same three things — senior team, proven process, quality first — so the words are worthless as a filter. What works is verification: shipped apps you can install, engineering thinking you can read, prices published before the sales call, and references you actually phone. This post is the checklist we would use if we were the ones buying, and yes — we are a Flutter agency, so at the end we run it on ourselves and show you where to check our claims. --- ### Why the usual research path misleads Search "best Flutter development company" and you will find listicles and directories. Both are useful and both need decoding. Many listicles are pay-to-play or affiliate-driven — inclusion is marketing, not merit. Directories like Clutch are more useful because verified client reviews are hard to fake at volume, but read the reviews, not the badges: one detailed review describing a real project tells you more than a dozen five-star ratings with two sentences each. Neither replaces the twenty minutes of direct verification below. Every criterion here is checkable from your desk before you ever book a call. ### Six things to verify, not ask about #### 1. Shipped Flutter apps you can hold Not screenshots — store listings. Open the portfolio, find the App Store and Google Play links, install two apps, and use them for ten minutes. Scroll fast, rotate, go offline, come back. A team that ships polished production Flutter cannot hide it, and a team that has not cannot fake it. Be suspicious of a portfolio that is all mockups and no links; be understanding about NDA work, but expect at least *some* named, installable proof. #### 2. Engineering thinking in public An agency that has genuinely wrestled a real-time feed or a payment flow can write three concrete pages about it; one that has not writes "we deliver scalable innovative solutions." Read the blog. Look for specifics: named trade-offs, numbers, things that went wrong. Open-source is the same signal, stronger — packages on pub.dev with real users mean the team's code survives public scrutiny, and you can read it yourself before you pay for more of it. #### 3. Who will actually build your app The people on the sales call and the people in your repo are often different people. Ask for names and profiles of the engineers assigned to *your* project, check that team pages show real humans with verifiable histories, and treat a refusal as an answer. Seniority matters more in Flutter than the pool's youth suggests: the framework is easy to start and unforgiving at production edges — rebuild scoping, platform channels, store review — where a senior has scars and a junior has tutorials. #### 4. Prices and timelines they will commit to in writing An agency that publishes ranges has decided its numbers can survive comparison; one that will only quote after a discovery call is keeping room to size the quote to the client. Same logic on the calendar: "How long is an MVP?" deserves a concrete range with conditions, not "it depends." It always depends — professionals tell you *on what*, and their conditions teach you how they think. #### 5. What a week of working with them looks like Demos on a cadence, a channel where you see progress daily, and honest reporting when something slips — ask each reference one question above all: **"When something went wrong, how did you find out?"** If the client discovered problems themselves, walk away. If the agency raised the flag first with a plan attached, that is the vendor you want, and no proposal deck reveals it. #### 6. What happens after launch Shipping is the midpoint of an app's cost, not the end — [maintenance has its own economics](https://nerdy.pro/blog/flutter-app-maintenance-cost). Verify there is a real post-launch offering: monitoring, store-policy updates, a named response path. And check code ownership in the draft contract, not the conversation: your repository from day one, your stores, your accounts. Any friction on this point is a rehearsal for a hostage negotiation. ### Five red flags that end the conversation 1. **Certifications as the sales pitch.** "HIPAA-certified developers" is a tell — [HIPAA has no such certification](https://nerdy.pro/services/flutter-app-development/healthcare), and an agency that leads with badges is betting you do not know that. Compliance-*literate* teams describe how they build instead. 2. **Yes to everything.** No pushback on scope, timeline, or feasibility during the sales process means the pushback arrives after the deposit, as change orders. 3. **A quote without questions.** Anyone who prices your app without interrogating scope, integrations, and the invisible 40% — auth flows, account deletion, store submission — is quoting a number designed to start a relationship, not finish a project. 4. **No engineers in the room.** If you cannot get a technical person into a pre-sales conversation, you are buying from a sales layer that will translate your product through a ticket queue. 5. **A portfolio without dates or store links.** Undated work can be a decade old; unlinkable work can be anyone's. ### Ten questions for the shortlist call 1. Which of your shipped Flutter apps is most similar to ours — and can we speak to that client? 2. Who exactly will work on our project, and what else are they on? 3. What would you cut from our scope, and why? 4. What is your estimate range for our MVP, and what moves it up or down? 5. How do you handle a payment flow / real-time feature / offline sync like ours? (Pick your hardest feature; listen for specifics.) 6. What does a normal week look like for us — demos, channels, reporting? 7. Tell us about a project that went wrong and what you did. 8. What do you *not* do, and where would you send us instead? 9. What does post-launch support cost and cover? 10. When can we see the repository, and whose name is on it? (Right answer: yours, from day one.) Question 8 is the quiet killer. A shop with no honest answer to "what don't you do" has never thought about what it is actually good at. ### Comparing quotes without fooling yourself Quotes for "the same app" legitimately vary 3–5× — mostly because they are not for the same app. Before comparing numbers, normalize the scope: which features, which platforms, whose backend, whose design, what testing, what store-submission work, what post-launch period. A structured breakdown of [what actually drives Flutter cost](https://nerdy.pro/blog/flutter-app-development-cost-2026) makes that normalization tractable. Then discard the cheapest bid unless you can explain *why* it is cheap — the usual answers are juniors, offshore leverage that vanishes in communication overhead, or scope that quietly excludes the invisible 40%. Cheap engineering that must be rebuilt is the [most expensive kind](https://nerdy.pro/blog/mobile-cost-reduction-case-studies); paying twice is the standard outcome of buying on price alone. Geography, while we are here: nearshore and offshore teams can be excellent — the variable that predicts outcomes is not the map, it is timezone overlap with your working day and the seniority of the people actually committing code. Insist on three-plus hours of overlap and named seniors, and the location question mostly answers itself. ### The same checklist, run on us We are Nerdy Production, a Flutter agency, and it would be strange to publish a vetting guide and exempt ourselves. So, line by line: our shipped work is in [the portfolio](https://nerdy.pro/portfolio) with store links where NDAs allow — install [ExtraETF](https://nerdy.pro/portfolio/extraetf) or [Jepta](https://nerdy.pro/portfolio/jepta) and judge the scroll performance yourself. The engineering thinking is on [this blog](https://nerdy.pro/blog) and in [open-source packages](https://nerdy.pro/open-source) on pub.dev you can read tonight. The people are on [the team page](https://nerdy.pro/team), and the person on your call is an engineer, usually the one who will architect your build. Prices and timelines are [published](https://nerdy.pro/services/flutter-app-development), with the conditions that move them. For references, ask us — we will connect you with named clients, the same ones quoted on this site. And question 8, answered in public: we do not do native-only builds, we [talk clients out of migrations](https://nerdy.pro/services/flutter-app-development/flutter-migration) their product does not need, and when [Kotlin Multiplatform](https://nerdy.pro/blog/flutter-vs-kotlin-multiplatform-2026) or [Expo](https://nerdy.pro/blog/flutter-vs-expo-2026) fits your team better, we say so in the comparison posts themselves. Run the checklist on us *and* on our competitors. If someone beats us on it, hire them. ### Frequently Asked Questions #### How much does it cost to hire a Flutter development company? Published agency tiers for a full build typically run from around fifteen thousand dollars for a lean MVP to ninety thousand and up for enterprise work — ours are public on our services pages, from MVP through e-commerce tiers, precisely so they can be compared. Hourly models for embedded senior engineers commonly land in the fifty-to-hundred-dollar range depending on geography and seniority. Treat any quote given without scope questions as a marketing number, and normalize scope before comparing bids — quotes for what sounds like the same app legitimately vary several-fold because the scopes differ. #### Should I hire a Flutter agency or a freelancer? A strong freelancer is the right call for a small, well-defined build with one hard skill at its center, and the wrong one the moment your product needs design, backend, and mobile work moving in parallel with continuity insurance. An agency's real product is redundancy and process: someone reviews the code, someone can take over mid-project, and delivery does not depend on one person's calendar or health. The honest cost comparison is not day rates — it is what a three-week stall in month four costs your launch. #### How do I verify a Flutter agency's portfolio is real? Install the apps. Real portfolio entries link to App Store and Google Play listings; open two, use them for ten minutes, and check the store reviews and update history while you are there — a listing abandoned since 2023 tells its own story. For NDA work, ask what specifically the agency built versus inherited. Cross-check the agency's name against client testimonials with real names and roles, and ask to speak to one of those clients directly; a confident shop treats that request as routine rather than an imposition. #### Is a cheap offshore Flutter team a false economy? Sometimes, and the failure mode is predictable enough to test for. Location does not write bad code — junior teams, thin review processes, and zero timezone overlap do, wherever they sit. Excellent teams exist across every geography and rate band; what you must verify is the same list as anywhere: named senior engineers on your project, installable shipped work, and at least three hours of overlap with your working day. What makes cheap expensive is the rebuild — code that must be rewritten at production quality costs more than building it right once. #### Fixed price or time and materials for a Flutter project? Fixed price fits a well-scoped first build: the agency carries estimation risk, you get a committed number, and the discipline of writing the cut list down benefits both sides — it is how we run MVP engagements. Time and materials fits ongoing evolution after launch, or genuinely exploratory work where fixing scope would be fiction. The arrangement to refuse is fixed price with vague scope, which converts every conversation into a change-order negotiation. A capable agency will tell you which model fits your situation rather than defaulting to the one that suits them. #### Can we switch Flutter agencies mid-project? Yes, and it happens more often than the industry admits — inherited codebases are a meaningful share of serious agencies' intake. The mechanics depend on two things you should secure on day one of the original engagement: repository ownership in your name and store accounts under your control. A competent successor will start with a paid audit of the existing code rather than a promise, and the audit report doubles as your negotiating position. Sometimes the verdict is two weeks of fixes, not a rewrite — a takeover should begin with evidence either way. --- *Shortlisting agencies right now? [Book a 30-minute call](https://nerdy.pro/contact) and bring this checklist — we'll answer all ten questions, including the ones that don't flatter us.* ## Flutter vs Expo in 2026: Framework vs Platform, Compared Honestly https://nerdy.pro/blog/flutter-vs-expo-2026 **TL;DR.** In 2026, comparing Flutter to "React Native" without saying Expo is comparing against a workflow almost nobody starts anymore — new RN projects are Expo projects. But that makes the comparison lopsided in an interesting way: **Flutter is a framework; Expo is a framework plus a commercial platform** — build service, update service, submission tooling. Choosing Expo is choosing that platform's conveniences and its dependencies. Choosing Flutter is choosing a self-contained toolchain and assembling the services yourself. Both are excellent. This post is about which *shape* fits your team. For the framework-level comparison — rendering, UI fidelity, ecosystems — read [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026) first; everything there applies here. This post covers what Expo adds on top, and what it costs. --- ### Where we stand, stated up front We are a Flutter-first shop — the [portfolio](https://nerdy.pro/portfolio) is the receipts — and we have shipped React Native work, modern Expo included. We also run our own delivery infrastructure, with CI/CD pipelines we own end to end, which colors how we weigh managed services against self-managed ones. Where that bias matters below, we flag it. --- ### What Expo actually is in 2026 Worth spelling out, because "Expo" means three different things people conflate: 1. **The framework layer** — Expo SDK (56 as of mid-2026, on React Native 0.85), a curated set of native modules, Expo Router for file-based navigation, and config plugins that generate the native projects so you rarely open Xcode or Android Studio. The Legacy Architecture is gone entirely; New Architecture is simply how RN works now. 2. **The development experience** — dev builds and the prebuild system. The old "Expo Go can't do real native modules" objection is history; a dev build includes whatever native code you need. 3. **The commercial platform** — EAS: cloud builds, store submission, and over-the-air updates as hosted, paid services. Layers 1 and 2 are open source and free. Layer 3 is a product with a pricing page, and it is where the comparison with Flutter gets genuinely interesting. --- ### Five dimensions that actually matter #### 1. Day-one to store-listing speed Expo is the best onboarding ramp in mobile development, full stop: one command to a running app, cloud builds without a local Xcode install, guided submission. Flutter's day one is also good — `flutter create`, `flutter doctor`, run — but store delivery is yours to assemble: signing, provisioning, CI, submission tooling. **Honest call:** for a solo developer or a web team shipping its first mobile app, Expo's ramp is materially gentler. For a team with mobile experience, the gap closes to near zero within the first sprint — the [store submission phase](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) is dominated by review cycles and listing assets either way, not by build tooling. #### 2. Over-the-air updates Expo's headline capability: EAS Update ships JavaScript-level changes to installed apps without store review — fixes in hours, staged rollouts, rollbacks. Within store policy (JS bundles are explicitly allowed; native code still goes through review), it is a real operational advantage. Flutter has no first-party equivalent. Code push for Flutter exists via third parties (Shorebird, founded by ex-Flutter leadership, is the serious one), but it is an add-on vendor decision, not a built-in. **Honest call:** if hotfix-without-review is a hard operational requirement, this is Expo's strongest single argument, and we say that as a shop that would love to tell you otherwise. Two moderating truths: store review in 2026 is usually measured in hours, not the days that made OTA feel essential; and teams routinely over-budget how often they will actually use it — a disciplined release train with good crash monitoring needs it rarely. #### 3. The dependency question This is the shape difference. An Expo project leans, by default, on EAS for builds, updates, and submission — hosted services with usage-based pricing that become part of your delivery path, and a plausible multi-hundred-dollar monthly line at team scale (their free tier is genuinely usable for small apps). You can self-host everything Expo does — builds locally, updates on your own server — but then you are operating the machinery the platform existed to spare you. A Flutter project has no platform in the loop: the toolchain is local and free, and delivery runs on whatever CI you already pay for. The cost is that *you* assemble and maintain that pipeline — which is real engineering time, and for a team without CI experience it is not a rounding error. **Honest call:** teams with existing DevOps capability get Flutter's independence nearly free. Teams without it are often better off paying EAS to make the problem disappear — an honest trade of money for operational surface, and our own bias toward owned infrastructure is exactly that: our bias, priced for a shop that ships apps weekly. #### 4. Native-code boundaries Config plugins and prebuild mean an Expo team can go remarkably far without touching native projects — and when you do need custom native work, you write a module and carry on; the wall people remember from 2021 is gone. Flutter's boundary is different: platform channels into real iOS/Android projects that are yours from day one, always in the repo, always openable. **Honest call:** near-parity in capability; the difference is philosophy. Expo abstracts the native projects until you opt in; Flutter hands them to you from the start. Teams with native engineers tend to prefer Flutter's transparency; teams without them tend to prefer Expo's abstraction holding longer. #### 5. Where each stack's gravity pulls - **Expo's gravity is the React web ecosystem**: one language with your web app, Expo Router publishing routes to web, React engineers contributing on day one. If your organization is React-shaped, Expo compounds advantages Flutter cannot match. - **Flutter's gravity is surface breadth and rendering ownership**: the same pixels on iOS, Android, [web where it fits, desktop, and embedded](https://nerdy.pro/blog/flutter-vs-native-2026), with the animation-heavy, design-system-owned UI story that made it the right call for [Arcana](https://nerdy.pro/portfolio/arcana) and [ExtraETF](https://nerdy.pro/portfolio/extraetf). **Honest call:** unchanged from [our RN comparison](https://nerdy.pro/blog/flutter-vs-react-native-2026), because it is the same underlying question: team shape and surface roadmap decide this, not tooling features. --- ### Pick Expo if… - Your team is React/TypeScript and you want them productive on mobile this week. - Hotfix-without-store-review is an operational requirement you will genuinely exercise. - You would rather pay a platform than staff a delivery pipeline — a legitimate trade, especially under five engineers. - Web-plus-mobile from one React codebase is the actual product shape. ### Pick Flutter if… - You own your design system and want identical rendering on every surface, or your UI is animation- and canvas-heavy. - You want a toolchain with no commercial platform in the delivery path — everything local, everything yours. - Your roadmap includes desktop or embedded, where Expo does not compete. - You are hiring dedicated mobile engineers rather than extending a web team — the calculus [our whole practice](https://nerdy.pro/services/flutter-app-development) is built on. --- ### The short decision framework **Expo is the right default for React-shaped organizations and for small teams that want delivery infrastructure to be someone else's job. Flutter is the right default for teams that own their design system, their pipeline, and a roadmap wider than two app stores.** If neither sentence clearly describes you, decide on the framework-level comparison — UI model, ecosystem, hiring — in [the RN post](https://nerdy.pro/blog/flutter-vs-react-native-2026), because the platform conveniences are the smaller half of a five-year decision. --- ### Frequently Asked Questions #### Is Expo production-ready in 2026, or is it just for prototypes? Fully production-ready, and the prototype reputation is years out of date. Expo is the default way new React Native apps are built: the SDK tracks React Native closely, dev builds include any custom native code you need, and the old Expo Go limitations stopped being the story long ago. The genuine 2026 question is not whether Expo can ship serious apps — it can — but whether you want its commercial platform, EAS, in your delivery path, which is a business decision as much as a technical one. #### Does Flutter have over-the-air updates like Expo? Not built in. Expo ships JavaScript-level updates to installed apps through EAS Update, within store policy, and that is a real operational advantage for hotfixes. Flutter's equivalent comes from third parties — Shorebird, built by former Flutter team leadership, is the credible option — as a separate vendor decision rather than a first-party feature. Worth sizing honestly before it decides your framework: store review in 2026 usually clears in hours, and teams with a disciplined release train reach for OTA updates far less often than they expect to. #### Is Expo free to use? The framework and tooling are open source and free, including building locally on your own hardware and even self-hosting updates. What costs money is EAS, the hosted platform most Expo teams actually use — cloud builds, store submission, over-the-air updates — which has a usable free tier and usage-based paid plans that a multi-app team should expect as a real monthly line item. The comparison with Flutter is therefore not free-versus-paid; it is paying a platform versus spending your own engineering time assembling the same delivery machinery. #### Can Expo do everything bare React Native can? In 2026, effectively yes. Config plugins and the prebuild system generate the native projects, dev builds carry arbitrary native modules, and ejecting as a scary one-way door is an obsolete concept — you can take over the native projects whenever you need to. The practical difference from Flutter is philosophical: Expo abstracts the iOS and Android projects away until you opt in, while Flutter puts them in your repository from day one. Teams with native expertise often prefer Flutter's transparency; teams without it benefit from Expo's abstraction holding longer. #### We're a React web team. Should we still consider Flutter? Consider it, but the honest default for you is Expo. Your engineers keep their language, their state patterns, and much of their tooling, and Expo Router can publish shared routes to the web — advantages Flutter structurally cannot offer a React organization. The cases where Flutter still wins for you: a product whose UI is heavily animated or canvas-driven, a design system that must render pixel-identically everywhere, or a roadmap that extends to desktop or embedded surfaces. If none of those apply, choosing Flutter would be choosing our preference over your leverage. #### Which is cheaper to build and run: Flutter or Expo? Build cost lands within roughly ten percent for comparable scope — framework choice moves budgets far less than scope and integrations do, which matches what we publish in our cost guide. The running costs differ in shape rather than size: an Expo team typically pays EAS subscription and usage fees in exchange for not operating build and update infrastructure, while a Flutter team pays in engineering time on CI it fully controls. Under five engineers without DevOps experience, Expo's trade is usually the better one; with an existing pipeline, Flutter's independence is close to free. --- *Weighing Expo against Flutter for a real product? [Book a 30-minute call](https://nerdy.pro/contact) — we'll tell you honestly which shape fits your team, even when the answer is Expo.* ## Flutter vs Kotlin Multiplatform in 2026: Two Different Bets, Compared Honestly https://nerdy.pro/blog/flutter-vs-kotlin-multiplatform-2026 **TL;DR.** Flutter and Kotlin Multiplatform are not two implementations of the same idea. Flutter is one bet: one rendering engine, one UI, every platform. KMP is a different bet: share the logic, keep each platform's native UI — unless you add Compose Multiplatform on top, at which point it becomes a Flutter-shaped bet with a younger ecosystem. Which one is right depends far more on your team's existing skills and your product's UI ambitions than on any benchmark. We build in Flutter and we will tell you below, specifically, when we would point you at KMP instead. If you just want the answer: skip to [the decision framework](#the-short-decision-framework). Comparing Flutter to React Native instead? That's [its own post](https://nerdy.pro/blog/flutter-vs-react-native-2026). To fully native Swift/Kotlin? [Also its own post](https://nerdy.pro/blog/flutter-vs-native-2026). --- ### Where we stand, stated up front We are a Flutter-first agency; the [work is public](https://nerdy.pro/portfolio). We have evaluated KMP for clients — the founder writing this has shipped production Kotlin for years and keeps current with KMP and SwiftUI — but we have **not shipped a production KMP app**, and it would be dishonest to pretend otherwise. So this post does two things carefully: where we compare developer experience, we say what our evaluation projects showed rather than implying production mileage; and where KMP is genuinely the better answer, we say so plainly, because a comparison written to always conclude "hire us" is worthless to you. --- ### What actually changed by 2026 KMP stopped being the experimental option a while ago, and a comparison that treats it as one is out of date. - **KMP itself is stable and endorsed from both sides.** JetBrains declared Kotlin Multiplatform stable in late 2023, and Google made it a first-class recommendation for sharing business logic on Android at I/O 2024. Netflix, McDonald's, and Cash App run KMP-shared code in production at serious scale. - **Compose Multiplatform reached iOS stability in 2025.** JetBrains' shared-UI layer is now stable on Android, iOS, and desktop, with the web target still in beta. This matters because it changes what "KMP" can mean: not just shared logic under native UIs, but shared UI too. - **The Swift side is the active frontier.** Kotlin's direct Swift export — no Objective-C bridging headers in between — shipped experimentally and is on JetBrains' path to stable. Until it lands everywhere, the Kotlin-to-iOS boundary still has friction native Swift developers notice: exported APIs that feel translated rather than designed. - **Flutter, meanwhile, did not stand still.** Impeller rendering is the settled default, and the framework's decade of ecosystem — pub.dev, tooling, hiring pool — is precisely the thing a younger stack competes against. --- ### The comparison most posts get wrong "Flutter vs KMP" is really **two different comparisons**, and collapsing them is how teams end up misled. **Comparison A: Flutter vs KMP with native UIs.** You share business logic — networking, storage, domain rules, view models — in Kotlin, and build the interface twice: SwiftUI on iOS, Jetpack Compose on Android. Typical sharing lands at 40–70% of the codebase. In exchange, each app is indistinguishable from fully native, because the UI *is* fully native. **Comparison B: Flutter vs Compose Multiplatform.** You share the UI too. CMP on iOS draws its own pixels through Skia-family rendering, exactly the architectural bet Flutter made in 2017 — at which point you are choosing between two implementations of the *same* idea, where Flutter's advantages are maturity, ecosystem depth, and platform breadth, and CMP's advantage is that the language and half your toolchain are the ones your Android team already lives in. Keep asking which comparison you are actually in. It decides almost everything below. --- ### Six dimensions that actually matter #### 1. UI strategy — the real fork in the road - **KMP with native UIs** delivers something Flutter architecturally cannot: every screen built from the platform's own components, with platform-perfect behavior in every scroll physics detail, context menu, and accessibility trait. If "indistinguishable from native" is a hard requirement — and in banking or health enterprise sales it sometimes contractually is — this is the honest way to get it while still sharing the logic underneath. - **Flutter** delivers the opposite guarantee: the same pixels everywhere, a design system you own outright, and one implementation of every screen. For brand-heavy, animation-heavy, custom UI — the [Arcana](https://nerdy.pro/portfolio/arcana) end of the spectrum — building it once beats building it twice on both cost and consistency. - **CMP** sits with Flutter on this axis, not with native. **Honest call:** this is the dimension to decide on first. Everything else is refinement. #### 2. Team shape — where most decisions are actually made KMP with native UIs still requires people who write SwiftUI and people who write Compose. You have removed duplicated *logic*, not the need for two platform skill sets. That is the right trade for organizations that already employ native teams — which is exactly who ships KMP at scale today. Flutter inverts it: one team, one language, both platforms — which is why a six-person shop like ours can carry [seven production apps](https://nerdy.pro/portfolio). If you are hiring from scratch and do not already own Kotlin expertise, Flutter is the smaller organization to build. **Honest call:** existing Android/Kotlin team → KMP deserves to be your default hypothesis. No mobile team yet → Flutter builds the smaller org. #### 3. Ecosystem and the iOS seam Flutter's ecosystem has a decade of depth: pub.dev packages for most SDKs that matter, mature tooling, an enormous body of production war stories (we have [written our share](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go)). KMP's library ecosystem is real and growing — kotlinx, Ktor, SQLDelight, Koin are solid — but thinner in the long tail, and the iOS seam is where evaluation projects feel it: until direct Swift export is stable everywhere, shared APIs consumed from Swift can feel like translated Kotlin rather than native Swift, and your iOS engineers will have opinions about that. **Honest call:** Flutter, clearly, today — with the honest footnote that this is KMP's fastest-moving front. #### 4. Incremental adoption — KMP's quiet superpower You cannot meaningfully adopt Flutter 10% at a time in an existing native app; add-to-app exists and works (we [run migrations on it](https://nerdy.pro/services/flutter-app-development/flutter-migration)), but it is a route to *replacing* the UI, not coexisting forever. KMP was designed for the opposite: extract one module — networking, sync, pricing rules — share it, ship, repeat. No rewrite, no big decision, reversible at every step. **Honest call:** for a healthy existing native app that wants to stop writing logic twice, KMP is the right tool and Flutter is the wrong one. We say the same thing on our [migration page](https://nerdy.pro/services/flutter-app-development/flutter-migration): stable native apps with happy teams should not be rewritten into anything. #### 5. Performance Boring answer, honestly held: for the overwhelming majority of apps, all three configurations are fast enough that architecture and rebuild discipline decide performance, not the framework. Flutter's Impeller and KMP's native UIs both render at platform speed; CMP's canvas rendering on iOS is the same approach Flutter spent years hardening, executed by a team earlier on that road. If your product lives at the rendering edge — streaming charts, heavy animation — Flutter's maturity there is proven; that was the [ExtraETF](https://nerdy.pro/portfolio/extraetf) decision. #### 6. Longevity risk Flutter's risk is concentration: it thrives at Google's pleasure, mitigated by open source and a massive installed base. KMP's risk is smaller and differently shaped: Kotlin is the official Android language, JetBrains' whole business is developer tooling, and Google co-endorses the sharing story — but CMP specifically is young, and betting shared UI on it is betting on its roadmap. Neither risk justifies fear; both justify writing business logic behind clean module boundaries, which is the actual insurance and is framework-independent — the same argument in [our pillar's FAQ](https://nerdy.pro/services/flutter-app-development). --- ### Pick Kotlin Multiplatform if… - You have an existing, healthy native app — especially Android-first with a Kotlin team — and the pain is writing logic twice, not the UI. - Platform-perfect native UI is a hard requirement and you can staff SwiftUI + Compose. - You want incremental, reversible adoption inside apps you already ship. - Your organization already thinks in Kotlin: server-side Kotlin, Android seniors, JetBrains tooling everywhere. ### Pick Flutter if… - You are building new and want one team shipping both stores — the economics that [our whole portfolio](https://nerdy.pro/portfolio) runs on. - You own your design system and want it pixel-identical everywhere, including [web and desktop where it earns its place](https://nerdy.pro/blog/flutter-vs-native-2026). - You want the deeper ecosystem and the larger dedicated-framework hiring pool today, not after the next roadmap milestone. - The alternative you are actually considering is CMP — in which case you are choosing Flutter's architecture either way, and maturity favors the original. --- ### The short decision framework **Existing native app, Kotlin skills in house, UI staying native: KMP — and you do not need an agency like ours to tell you that. New product, one team, custom design system, both stores on one budget: Flutter. Tempted by Compose Multiplatform on a new product: understand you are making Flutter's bet with a younger stack, and make the choice on ecosystem maturity, not language preference.** If you are still torn, the tiebreaker is the team you will actually employ in two years. --- ### Frequently Asked Questions #### Is Kotlin Multiplatform production-ready in 2026? Yes, without qualification for shared business logic: KMP has been stable since late 2023, Google recommends it for sharing logic on Android, and companies like Netflix, McDonald's, and Cash App run it in production at scale. Shared UI is the more nuanced half: Compose Multiplatform is stable on Android, iOS, and desktop as of 2025, but its iOS ecosystem is young compared to Flutter's, and the web target is still in beta. Production-ready and battle-hardened are different claims, and for shared UI on iOS the second one is still being earned. #### Does Kotlin Multiplatform replace Flutter? No — in its classic form it answers a different question. KMP shares business logic while each platform keeps its native UI, so you still build two interfaces and staff two UI skill sets; Flutter shares everything including the pixels, so one team ships both stores. The configurations converge only if you adopt Compose Multiplatform for shared UI, at which point you have chosen Flutter's architectural bet — canvas rendering, one widget tree — implemented by a younger stack. Different bets suit different teams; neither replaces the other. #### Can you share UI code with Kotlin Multiplatform? Yes, through Compose Multiplatform, which is stable on Android, iOS, and desktop, with web still in beta. On iOS it draws its own pixels through Skia-family rendering rather than using Apple's UI components — the same architecture Flutter uses, which is why a team choosing CMP for a new app should compare it against Flutter directly: the trade is Kotlin familiarity and Android-Compose skill reuse against Flutter's decade of ecosystem, tooling, and production hardening on exactly that rendering approach. #### We already have an Android app in Kotlin. KMP or Flutter? Almost certainly KMP, and a Flutter agency telling you otherwise is selling, not advising. Your Kotlin code, your team's skills, and your existing architecture carry over directly: extract shared modules incrementally, add an iOS app that reuses the logic, and keep every step reversible. Flutter would mean rewriting working code and retraining a working team — a cost that only pays off if you also want what Flutter uniquely offers, such as one owned design system across every platform or a consolidation away from platform teams entirely. #### Is Compose Multiplatform better than Flutter for a new app? For most new products in 2026, Flutter remains the safer version of the same bet. Both draw their own UI on every platform; Flutter has run that architecture in production since 2018 with the package ecosystem, devtools, and hiring pool to show for it, while CMP reached iOS stability in 2025 and is still building that depth. The case for CMP over Flutter is organizational, and it is real: a strong Kotlin team that writes Jetpack Compose daily keeps its language, IDE, and muscle memory. Weigh team continuity against ecosystem maturity — that is the actual decision. #### Do you build Kotlin Multiplatform apps? Not in production, and we say so rather than bluff: our shipped work is Flutter, plus native and backend engineering around it. We have evaluated KMP seriously — including for clients whose situation pointed that way — and when the evaluation says KMP is your answer, we tell you and help you scope what to share first, because an honest no builds more trust than a stretched yes. If you want the comparison run against your specific product and team, that conversation is exactly what our discovery call is for. --- *Deciding between Flutter and KMP for a real product? [Book a 30-minute call](https://nerdy.pro/contact) — we'll give you an honest recommendation, including "use KMP, and you don't need us."* ## Five States That Can't Exist, and Your Code Allows All of Them https://nerdy.pro/blog/impossible-states-dart-sealed-classes Here is a task you have written twenty versions of: a product list screen. Load from the server, handle the error, render the list. We handed it to an agent — Claude Opus 5 — and this came back. ```dart class ProductsState { final List? items; final bool isLoading; final Exception? error; } ``` That looks like something you would write yourself. Which is exactly the point: these models were trained on what all of us have been writing by hand for years. Now count. Three fields: a bool, a nullable list, a nullable error. Each has two states you would care about. Two times two times two — **eight combinations.** How many of them mean anything? | `isLoading` | `items` | `error` | What it means | | --- | --- | --- | --- | | `true` | `null` | `null` | Loading | | `false` | `null` | set | Failed | | `false` | set | `null` | Loaded | | `true` | set | `null` | ? | | `true` | `null` | set | ? | | `true` | set | set | ? | | `false` | set | set | ? | | `false` | `null` | `null` | ? | Three. The other five are not compile errors. They are valid code. It builds, it passes review, it ships, and it sits there. `isLoading` is true and there is an error next to it — what do you render? Or everything is empty: not loading, no list, no error. Have we not started yet, or did we go and come back with nothing? The code does not know. It guesses, or the next person to open the file does. ### Key takeaways - **An impossible state is one your type permits and your domain forbids.** It is the same thing as a broken invariant, seen from the other side. - **The cost is not runtime bugs.** It is a convention nobody wrote down, branches nobody ever considered, and a tax you pay at every read rather than once per incident. - **The invariant always exists.** The only question is whether a person is holding it — a comment, a wiki page, an `assert` that does not run in release — or whether breaking it does not compile. - **`enum Status` plus nullable fields makes it worse**, not better: twelve combinations instead of eight, and still three that mean anything. - **A sealed class with non-nullable fields leaves exactly the meaningful states** and makes the rest unbuildable. No booleans, no nullables, no `!`. - **Exhaustiveness is the payoff.** Add a fourth state and the compiler lists every place that does not handle it — which is feedback an agent can act on, not a wall it hits. - **One `_ =>` branch switches all of that off, silently.** That is the single review rule worth taking from this article. - **Algebraic data types do not fix everything.** They eliminate impossible combinations of states, not impossible values, and they do nothing at a system boundary. ### Impossible states, and the older name for them Vocabulary first, because two words get used for one thing. **An impossible state is one your type permits and your domain forbids.** A screen cannot be loading and showing an error at the same time. The type allows it. That is a hole. The older and more precise name for it is **invariant** — a rule that has to hold at all times. *If we are loading, there is no data yet. If there is an error, it has a message.* An impossible state is just a broken invariant. Same fact from two sides: the invariant is how it is supposed to be, the impossible state is what you got when it was not. Hold on to that word. It is where this turns. #### Why this is a problem, and why the usual explanation is wrong The usual line is that impossible states are bugs that do not crash — they quietly render the wrong screen. That sounds convincing, and if it were true the answer would be cheap: tests, QA, an alert on the empty screen. Catch it, fix it, move on. That is not the problem. It is not about runtime at all. There are three costs, and all three are in your code **right now**, even while the app behaves perfectly. **One: the convention nobody wrote down.** Look at the agent's class again. Where does it say that when `isLoading` is true you should not read `items`? Nowhere. Where does it say `error` and `items` are never both populated? Nowhere. Those rules exist and people rely on them, but they are not in the type and not in a comment. They live in the head of whoever wrote the class, and everyone after that reconstructs them from the code, reading the `if`s and inferring intent. Five minutes a file, for every person who opens it. **Two: the number of branches.** Eight combinations means eight cases, each of which somebody has to either handle or deliberately rule out. Three get handled. The other five are neither handled nor ruled out — they were **never considered.** That distinction matters: a ruled-out case is a decision, an unconsidered one is a hole. And the count grows by multiplication. | Fields that travel together | Combinations | Meaningful | | --- | --- | --- | | 3 | 8 | 3 | | 4 | 16 | 3–4 | | 5 | 32 | 3–4 | | 6 | 64 | 3–4 | **Three: you pay at every read, not once per bug.** This is the important one. A bug gets fixed once. A convention that is not in the type gets re-derived by everyone who touches that screen — at review, when adding a feature, while digging through an incident. That is not a one-time cost, it is a tax. Which is also where the usual patch comes from. People plug these holes with conditions: check we are not loading here, check the error is not empty there. Every one of those `if`s is a patch over a hole you cut yourself when you declared three independent fields. Five holes, usually two patches — the ones that already fired. And note, in passing: none of this **crashes.** No crash report, no stack trace, nothing in your error tracker. So "monitoring will catch it" does not apply. But that is a side effect, not the point. What changed over the last couple of years is **volume.** This class used to appear once a week, written by a human who spent at least thirty seconds thinking about the fields. Now it appears in twelve seconds and nobody has read it — not the agent, not you. A reviewer is a person looking at three fields, not somebody holding eight combinations in their head. #### Who is holding the invariant? So: the question is never whether you have an invariant. You always do. The question is **who is responsible for it.** Usually a person. A comment above the class. A wiki page nobody has opened since the year before last. A code review convention that two people out of five remember. At best an `assert` in the constructor. > **An assert is not an invariant in Dart** > > Dart `assert` statements are stripped in release builds. Not "skipped" — the condition is never evaluated, and neither are its arguments, which is why an expensive check inside an assert costs nothing in production. The practical consequence is the part people forget: an invariant you defended with an assert does not exist where your users are. It held during development, where you were already watching, and it is absent in the one place you are not. Asserts are a debugging aid. They are not a constraint. All of that is an invariant somebody is **holding** — by hand, by attention, by memory. The alternative is to arrange things so there is nothing to hold, because breaking it is not expressible in the type. Not "we check that this cannot happen" but "this does not compile." And here is why that stopped being an academic distinction. An invariant held by a human works exactly as long as code is written at human speed. An agent has not read your comment, has not opened your wiki, was not in your code review. **Conventions do not scale to generation speed. Compilers do.** ### Algebraic data types, in one pass One disclaimer before the theory, to kill half the objections. The example here is deliberately the most obvious one possible. Loading, error, success is the single most over-used illustration of algebraic data types on the internet — it is in every tutorial. The idea has a famous slogan, *make illegal states unrepresentable*, usually credited to Yaron Minsky's *Effective ML* talks, and it has been around for well over a decade. Nothing here is a discovery. The interesting part is where that class at the top of this article came from, and what you do when code like it arrives in batches. Now the definition. No category theory. **A product is "and".** An ordinary class with fields: a string **and** a number **and** a bool. The number of values it can hold is the product of the counts. That is where the eight came from — it is not rhetoric, it is arithmetic. Every field you add **multiplies.** **A sum is "or".** The value is either this or that, and there is no third option. The simplest sum everybody already knows is an `enum`. A sealed class is the same sum, except each case can carry its own data. Here the counts **add**: loading is one value, failed is however many errors exist, loaded is however many lists exist. Nothing gets multiplied for free. The whole idea in one sentence: **you cannot construct the type in a way that represents a state that was never valid.** #### "Why not just an enum?" The obvious objection is that you do not need sealed classes for this — add an `enum Status` with three values and stop overthinking it. It is a reasonable instinct, and it does not work. ```dart enum Status { loading, error, success } class ProductsState { final Status status; final List? items; final Exception? error; } ``` Count again: three status values, times two list states, times two error states. **Twelve combinations.** It was eight. Still three that mean anything. Arithmetically it got **worse** — nine pieces of garbage instead of five. And the original problem is untouched. `status` is `success` and `items` is `null`: compiles fine. In the UI you are still writing `items!`, swearing to the compiler that the data is definitely there. A promise is not a guarantee. It is a disabled check with pleasant syntax. The difference is what the sum carries. An `enum` is a sum **without data** — it tells you which state you are in, but the data sits alongside it, tied to the state only by your discipline. A sealed class is a sum **with data**: the list exists exactly where it means something, and nowhere else. So the rule is: if your states carry no data, use an `enum`, it is shorter. The moment even one state has a payload, `enum` stops helping, and **every `!` in your UI is the receipt.** This is an old idea. ML, where it comes from, is fifty years old; Haskell, OCaml and F# have had it for decades. In the mainstream it is now essentially everywhere — Dart got it in version 3, in 2023. What changed is not the idea but the argument for it. ADTs used to be sold on elegance, and aesthetic arguments lose to sprints. The argument now is that **code is written faster than it is read**, the only reviewer keeping up with generation is the compiler, and anything you did not express in the type you expressed as a hope. ### The rewrite Three steps, about two minutes of work. **Step one: write the states down in words, before any code.** The screen is loading. The screen failed. The screen has a list. That is it — three. If you end up with seven at this step, you are almost certainly writing down flag combinations rather than states. A state is something you can name in one word and point at on a mockup. **Step two: each state becomes its own class.** ```dart sealed class ProductsState { const ProductsState(); } final class Loading extends ProductsState { const Loading(); } final class Failed extends ProductsState { const Failed(this.error); final Exception error; } final class Loaded extends ProductsState { const Loaded(this.items); final List items; } ``` Look at what happened. `Failed` holds an error, and it is **not nullable** — the state "we failed but there is no error" no longer exists, and you physically cannot construct it. `Loaded` holds a list, also non-nullable. `Loading` holds **nothing**, because during loading there is no data, and keeping a field for it is precisely an invitation to an impossible state. Notice what is not there. **Not one boolean. Not one nullable field.** That is the entire refactor; the rest is syntax. One Dart-specific detail: `sealed` restricts subtyping to the **same library**, which in practice means the same file unless you are deliberately splitting it with `part`. That is not pedantry, it is the mechanism. To check exhaustiveness the compiler has to know the complete list of cases, and it can only guarantee that within the library. Hence the convention — however many cases you have, they live together in one file. It looks odd the first time and then turns out to be convenient: the screen's whole state space is visible at a glance. **Step three: the UI.** ```dart Widget build(BuildContext context) { return switch (state) { Loading() => const AppSpinner(), Failed(:final error) => ErrorView(error), Loaded(:final items) => ItemList(items), }; } ``` Not one `if`. Not one null check. The data is destructured in the pattern — `:final items` — and arrives already typed. Inside the `Loaded` branch the list **exists**, rather than "might exist." Compare that to what you would have on flags: a chain of checks whose order matters and whose reason nobody remembers. #### Doing this on a project with forty screens Do not rewrite everything. The rule: **new screen, sealed from the start; old screen, when you are already in there fixing something.** A screen nobody has touched in two years, leave alone. It may well have impossible states, but they have either already fired or never will. For the ones you do touch, priority comes down to one thing: **how many places read that state.** One `switch` in one widget — rewriting barely pays, since you can see everything anyway. A state read from six places, across the UI, analytics, logging and a push handler, is where exhaustiveness earns its keep, because those are exactly the five out of six that get forgotten. If a screen already funnels its state through a single stream rather than [a pyramid of flags](https://nerdy.pro/blog/ai-agents-struggle-with-flutter), that is the cheapest place to start. ### The same type in four other languages Not because you need a second language, but so it is clear this is not a Dart peculiarity or a Flutter-community fashion. Same type, three states, data attached to the state. **Kotlin.** ```kotlin sealed interface ProductsState data object Loading : ProductsState data class Failed( val error: AppError, ) : ProductsState data class Loaded( val items: List, ) : ProductsState ``` `when` used as an expression has to cover every branch — exactly our `switch`. **Rust.** More interesting, because in Rust this is the built-in `enum`: ```rust enum ProductsState { Loading, Failed(AppError), Loaded(Vec), } ``` In Rust an `enum` **is** a sum with data from the start, with no extra ceremony. Miss a case in a `match` and it does not compile. **Swift**, the same thing with associated values: ```swift enum ProductsState { case loading case failed(AppError) case loaded([Product]) } ``` **And the exotic one, Idris** — the ML syntax all of the above descend from: ```idris data ProductsState = Loading | Failed AppError | Loaded (List Product) ``` Look at the vertical bar. It reads as "or". **The sum type is literally written with an or-symbol** — this is a sum long before anybody called it a sealed class. Idris also does something none of the other four can: ```idris import Data.Vect data ProductsState : Type where Loading : ProductsState Failed : AppError -> ProductsState Loaded : Vect (S n) Product -> ProductsState ``` `Vect (S n)` in the type means a list **guaranteed to have at least one element.** Not "we checked", not "we agreed" — you simply cannot construct `Loaded` with an empty list. Remember this one; it comes back at the end. One idea, five languages, exhaustiveness in all of them. If you are not on Dart, everything below works the same for you. ### Exhaustiveness, which is what all of this was for A month later a requirement arrives: when the list comes back empty, show a placeholder with a button. That is not an error and it is not success-with-data. It is a fourth state. ```dart final class Empty extends ProductsState { const Empty(); } ``` One line. Nothing else changed. And the project does not build: ```text The type 'ProductsState' is not exhaustively matched by the switch cases since it doesn't match 'Empty()'. ``` The compiler lists **every** place the new state is not handled. Not one of them — all of them. Now, why this belongs in an article about AI-written code. "Add an empty state to the list screen" is exactly the size of task people hand to an agent without looking. The agent will add the class, update the `switch` it had in context, and **not update the other two** — the one in analytics and the one behind the refresh button. They were not in the prompt, so as far as it is concerned they do not exist. On flags, that ships. There is no diff to catch it, because the diff only contains what the agent changed. Tests are green, because a state that did not exist a month ago has no tests by definition. You find out from a user six weeks later, in the form of "sometimes I just get a blank screen." On sealed classes it ships nowhere. It does not build. And here is the part worth taking away. **Exhaustiveness is not protection from AI. It is feedback for it.** What makes an agent bad at this is that it has no access to the list in your head of "and also do not forget this bit over here." The compiler emits that list in machine-readable form, with files and line numbers. The agent hits red and fixes all three, which it is genuinely good at, because it was told exactly where. You are not stopping it from working — you are giving it the thing it was missing. The same mechanism helps a human once a week and an agent every twenty minutes. #### The one line that switches it all off ```dart return switch (state) { Loading() => const AppSpinner(), _ => ItemList(state.items), }; ``` One underscore and exhaustiveness is dead. Quietly, with no warning. A catch-all handles everything by definition, so there is nothing left for the compiler to check. Add a fifth state, a sixth, a tenth — it builds, and silently falls through to here. If you take exactly one review rule out of this article, take this one: > **A switch over a sealed type should not have a default branch.** It is the one line here you actually have to check by eye — and a lint rule catches it, so you do not even have to. ### Where algebraic data types do not help Four places where they do not work, or work against you. Without this section the rest is a sermon. **A sum does not cure a product.** You split the screen into three states, which is good. But `Loaded` is still an ordinary class with fields, and if there are twelve of them and half are nullable, you have moved the swamp down one floor. ADTs are about **which states exist**, not about what is inside a state. **States that overlap — the most common mistake people make when they migrate.** A pull-to-refresh requirement lands: the list is already on screen and you are fetching a fresh one. Is that `Loading` or `Loaded`? The naive answer is a `Refreshing` case with `items` in it. A week later you have `Refreshing`, `RefreshingAfterError` and `FailedButHasCache` — the same combinatorial explosion, now in classes, which is worse than flags because it is more verbose. The right answer upsets people: ```dart final class Loaded extends ProductsState { const Loaded(this.items, {this.isRefreshing = false}); final List items; final bool isRefreshing; } ``` Yes, the boolean is back, and that is **fine.** It now lives inside a state where it means something: "we have a list and we are refreshing" is a real situation you need to express. And "refreshing with no list" no longer exists, because there is nowhere to put it. > **Sums for what is mutually exclusive, products for what co-occurs** > > This is the rule that decides whether the refactor helps or hurts. Two things that can never be true at once want a sum — separate cases of a sealed class. Two things that are routinely true at the same time want a product — fields sitting side by side inside one case. Getting those backwards is the main way to end up worse off than you started: mutually exclusive states modelled as parallel booleans give you the eight-combination problem, and co-occurring facts modelled as separate cases give you a class per combination. **Types do not enforce every kind of invariant, and this matters more than the rest.** A sealed class expresses a **structural** invariant beautifully — which states exist, and which data travels with which state. It expresses nothing about invariants **on values**: this list is sorted, the start date precedes the end date, the line items sum to the total, this string is a valid email. All of those are still held by a person, and no amount of `sealed` helps. That Idris snippet is exactly the counter-example. `Vect (S n)` — non-empty, in the type — is a value invariant pushed into the type system. The technique is called dependent types, and you could express "sorted" and "start before end" the same way. The price is a language you do not write in and, realistically, will not. In practice you reach for a different technique instead: a private constructor with validation, so a value of it cannot be created in an invalid shape. The same bucket holds **system boundaries.** JSON off the wire is not typed. ADTs start *after* parsing, and a sealed class will not save you when the backend sends `"succes"` with one "s" — explicit deserialization with hand-written keys will. The sentence worth keeping: **ADTs eliminate impossible combinations of states, not impossible values.** **And over-engineering.** A sealed class with two cases and no data is an `enum`, and the `enum` reads faster. A sealed class with one case is a class. An honest boolean stays an honest boolean — `isSelected` on a checkbox has exactly two states, both valid, and an ADT improves nothing there. The test is simple: count the combinations and cross out the meaningless ones. **Nothing to cross out, leave it alone.** Finally, the honest cost: **it is verbose.** Four states in Dart is about thirty lines, against four fields in the original. The "slower to write" half of that argument does not survive 2026 — that is precisely the work an agent does in seconds without mistakes. The "slower to read" half survives, and that is the price you pay. ### Find yours in thirty minutes #### Reviewing a state class Ten checks in three groups. Anything unchecked is a decision somebody has not made yet. ##### The type itself - Mutually exclusive situations are separate cases of a sealed type, not parallel booleans - Every field inside a case is non-nullable, or the null means something specific and documented - Data lives on the state that owns it, not alongside a status enum - Facts that co-occur are fields inside one case, not a case per combination ##### Every switch over it - No default branch and no underscore pattern over a sealed type - No null-assertion operator on a field the state is supposed to guarantee - Every reader of the state is covered, including analytics, logging and background handlers ##### What types cannot do for you - Value invariants — sorted, non-empty, in range, well-formed — are enforced in a constructor, not assumed - Parsing at the system boundary validates explicitly rather than trusting the shape - Asserts are treated as a debugging aid, since they do not run in release builds ### The short version An impossible state is one your type permits and your domain forbids, which is the same thing as a broken invariant. It is expensive not because it breaks something at runtime, but because every convention that is not in the type gets re-derived by somebody at every single read. The invariant always exists. The only question is whether a person is holding it — with a comment, a wiki page and an assert that does not run in release — or whether there is nothing to hold, because breaking it does not compile. Three fields that travel together, eight combinations, three of them meaningful. A sealed class with non-nullable fields leaves exactly those three and makes the other five unbuildable. Exhaustiveness is the payoff: it turns "somebody forgot to update the handling" into "the project does not build", and it works the same regardless of who forgot. A default branch switches all of it off, quietly. Here is the larger shift. Types used to be how you explained your thinking to a colleague opening the file six months later. Now they are the only way to explain it to something writing code faster than you can read it. The craft did not go anywhere — it moved up a level. You used to write the implementation. Now you write the boundaries the implementation has to stay inside, and anyone can fill them in, including a machine. ### Frequently asked questions #### What is an impossible state in programming? A state your type permits and your domain forbids. A screen state class with a loading boolean, a nullable list and a nullable error has eight combinations, but only three of them describe a situation the product can actually be in. The remaining five compile, pass review and ship. The same idea is more precisely called a broken invariant: the invariant is the rule that has to hold at all times, and the impossible state is what you get when the type does not enforce it. #### What are algebraic data types? A way of describing how many values a type can hold. A product type is and — an ordinary class with fields, whose value count is the product of its field counts, so every field you add multiplies. A sum type is or — the value is one case or another and there is no third option, so the counts add instead of multiplying. An enum is a sum without data; a sealed class is a sum where each case carries its own data. The point of using sums is that you cannot construct a value representing a state that was never valid. #### Why not just use an enum with a status field? Because it makes the arithmetic worse. An enum with three values next to a nullable list and a nullable error gives twelve combinations rather than eight, and still only three of them mean anything. The original problem is untouched: status can be success while the list is null, that compiles, and the UI ends up asserting that the field is non-null. An enum tells you which state you are in but leaves the data alongside it, tied to the state only by convention. A sealed class attaches the data to the state, so the list exists exactly where it means something. #### Do sealed classes in Dart have to live in one file? In the same library, which in practice means the same file unless you deliberately split it with part. That is the mechanism rather than a style rule: to check exhaustiveness the compiler needs the complete list of subtypes, and it can only guarantee completeness within the library. The convention that follows is to keep every case of a state together in one file, which turns out to be convenient — the whole state space of a screen is visible at a glance. #### Why does a default branch break exhaustiveness checking? Because a catch-all handles every case by definition, so there is nothing left for the compiler to verify. Once a switch over a sealed type has a default or underscore branch, adding a fifth or a tenth state compiles cleanly and silently falls through to that branch instead of failing the build. This is the single highest-value review rule on the subject: a switch over a sealed type should not have a default branch, and a lint rule can enforce it. #### Do sealed classes help with AI-generated code? Yes, but not as a barrier. Exhaustiveness is feedback rather than protection. An agent asked to add a new state will update the switch it had in context and miss the ones it did not, because they were not in the prompt. With flags that ships silently, since the diff only shows what changed and a brand-new state has no tests. With a sealed type the build fails and the compiler names every unhandled location with a file and a line number, which is exactly the input an agent is good at acting on. #### Where do algebraic data types not help? Four places. They do not fix a case that is itself a bloated product type with twelve nullable fields. They hurt when used for states that genuinely overlap, such as refreshing while data is already on screen, which wants a boolean inside the loaded case rather than a new case. They express nothing about value invariants such as sorted, non-empty or a valid email, which still need a validating constructor, and they start only after parsing, so they do nothing at a system boundary either, where the backend sends a misspelled status string. And they turn into over-engineering wherever an enum or an honest boolean would have done. If you want the wider list of what unsupervised agents do to Flutter code, this is number three on our [seven-item breakdown](https://nerdy.pro/blog/ai-agents-struggle-with-flutter), and the process we use to keep it from happening is written up in [how we build with AI](https://nerdy.pro/blog/building-with-ai). This article is the companion to our video episode on the same subject; the previous one was [your SSL pinning probably is not working](https://nerdy.pro/blog/ssl-certificate-pinning-flutter). Run the exercise and tell us how many rows you crossed out, and on which screen. Our bet is that most people land on the same three states we did. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that builds and maintains apps across fintech, healthcare, and retail. We also run an [AI code audit](https://nerdy.pro/services/ai-code-audit) for teams whose codebase grew faster than anyone could review it.* ## Your SSL Pinning Probably Isn't Working https://nerdy.pro/blog/ssl-certificate-pinning-flutter Google's own Android documentation says certificate pinning **"is not recommended for Android apps."** Apple says **"in most cases, pinning is not necessary and should be avoided."** And here is a Chromium engineer answering a question about how to set pinning up properly. He opens his reply like this: **"While pinning in general should never be encouraged, as it actively harms the security and stability of the Internet at large, if you're pinning to a private CA, Android's Network Security Config is the preferred approach."** He is literally telling you how to do it, and he still cannot help himself. Meanwhile, pinning ships in about half the mobile apps we look at, including the ones that land on our desk for an audit. Good news: if you are on Flutter, chances are it is not working. Bad news: same sentence. ### Key takeaways - **Pinning is a narrow control against a threat that has shrunk**, with a blast radius the size of your entire user base. Certificate Transparency, CAA records and the Android 7 trust-store change took most of its original job away. - **It is genuinely necessary for finance, health and government** — apps that have to verify against OWASP's MAS-L2, the defence-in-depth profile for software handling sensitive data. For everyone else, Google, Apple, Cloudflare and half of OWASP say don't. - **Certificate lifetimes are collapsing**: 200 days as of March 2026, 100 in 2027, 47 in 2029. If you pin a leaf and cannot rotate without an app release, that is eight forced releases a year. - **Pin the key (SPKI), never the certificate**, and always ship a backup pin for an offline key you control. - **Never ship straight to hard-fail** — and build your own failure telemetry, because neither platform has any. - **On Flutter, `` in `network_security_config.xml` does nothing for Dart traffic.** Dart runs its own TLS stack. The bug has been open since January 2022. - **The 30-minute test** at the end of this article tells you which of these describes your app. The naive version of that test lies to you. ### What pinning actually protects you from Quick baseline, so we are using the same words. Normally your app trusts any certificate signed by any certificate authority in the system trust store, and that store holds around a hundred and fifty root certificates. Pinning narrows that to something specific: **this key, and nothing else.** A note on the name first. Everyone says "SSL pinning," us included, in the title of this article. **SSL has been dead for over a decade** — it is all TLS now. But that is what people search for, so it stays. Just know that if somebody says "SSL" and means it in 2026, that tells you something about how fresh their sources are. Now the honest threat model. Two things pinning genuinely defends against: **A compromised or rogue certificate authority.** This is pinning's actual job. The textbook case is DigiNotar in 2011: attackers breached a Dutch CA, issued fraudulent certificates for Google domains, and intercepted Iranian users. Pinning is what caught it — Chrome shipped a preloaded pin for google.com. **Corporate interception.** DLP proxies, antivirus software doing TLS inspection, an MDM installing a root at the system level. Pinning breaks all of them. That is the point — but it also means you break your enterprise customer's legitimate proxy along with the attacker's. And now the part the blog posts from 2015 do not cover, because none of it existed yet. #### The rogue-CA threat has shrunk a lot **Certificate Transparency** publishes every publicly trusted certificate to open, append-only logs. The mechanism launched in 2013, but it became mandatory in practice in 2018: Chrome started enforcing it in version 68, and Apple enforces it **at the OS level**, which means inside apps, not just in Safari. That is not theory. September 2015 was the first time CT caught a misissued certificate in the wild — an unauthorized certificate for google.com that Symantec produced during internal testing. No malicious intent, valid for a single day. It was also the first crack in what ended, in 2018, with browsers distrusting Symantec entirely. A recent one surfaced in September 2025: the Croatian CA Fina had issued twelve unauthorized certificates for Cloudflare's 1.1.1.1, spread across February 2024 and August 2025. Nobody noticed for eighteen months; CT logs are what finally turned them up. And note — Fina is not in the mobile trust stores at all, so those certificates would never have validated on a phone anyway. The log caught them regardless of who trusts the CA. That is exactly the value. On top of that: CAA records, which are DNS records declaring which CAs may issue for your domain, plus mandatory multi-perspective domain validation. #### The platform already did some of this for you Starting with Android 7 — more precisely, `targetSdk` 24 — apps **do not trust user-installed certificates** by default. That has been the default since 2016. So the "victim installed a malicious root" scenario is not something pinning saves you from any more. The operating system beat you to it. | `targetSdk` | System CAs | User-installed CAs | Cleartext HTTP | | --- | --- | --- | --- | | ≤ 23 | trusted | **trusted** | allowed | | 24–27 | trusted | not trusted | allowed | | ≥ 28 | trusted | not trusted | **blocked by default** | That table is keyed on `targetSdk`, not on the device's Android version — which is why an old app on a new phone still trusts whatever the user installed. #### What pinning does not protect against at all OWASP's framing is blunt: if an attacker controls the device, they simply disable your pinning logic. On a rooted Android or a jailbroken iPhone that is one command in `objection` — and for Flutter there is dedicated tooling, which we will get to, because it is interesting. So: **pinning protects your real users from interception on a hostile network. It does not protect your API from the owner of the device.** If you are using pinning to hide keys or to stop reverse engineering, you have already lost. That is what server-side attestation is for — Play Integrity, App Attest — not client-side tricks. > **OWASP contradicts itself here, and it matters when someone hands you a report** > > MASVS does require pinning — but only for an app that must verify against MAS-L2, OWASP's defence-in-depth profile for software handling sensitive data. It is not a baseline control. The testing guide says plainly that if an app does not implement pinning, that should not be reported as a vulnerability. And the OWASP Pinning Cheat Sheet says: "The first question should be, should I pin? The answer to this is probably never." That language was added in March 2023; it was not there before. So when a pentest report flags "missing SSL pinning" as a finding, that is usually an automated checklist, not a decision about your threat model. ### Do you actually need certificate pinning? Short checklist. If any one of these is you — **do not pin.** - You do not control both ends: your server and your app - You cannot update the pin set safely and quickly - Updating pins requires an app release - You cannot know the key pair before it goes into production - It is not a native mobile app Who genuinely needs it: finance, health, government services — anywhere the data is sensitive and the threat model assumes a hostile network. That is the MAS-L2 case. Everyone else: probably not. #### Why this got urgent in 2026 In April 2025 the CA/Browser Forum voted to phase down TLS certificate lifetimes. Apple filed the ballot. The schedule: | From | Maximum lifetime | Rotations per year | | --- | --- | --- | | before March 2026 | 398 days | ~1 | | **15 March 2026** | **200 days** | ~2 | | 15 March 2027 | 100 days | ~4 | | 15 March 2029 | 47 days | ~8 | The 200-day tier is in force right now. Forty-seven days is roughly **eight certificate rotations a year**. If you pin the leaf and you cannot update pins without shipping, that is eight forced app updates a year — with store review, and with users who do not update. And the ecosystem is fighting pinning on purpose. Back in 2024, Let's Encrypt started **picking the issuing intermediate at random**, specifically to break the habit of pinning intermediates. In November 2025 it carried the same policy into a new hierarchy of six intermediates, and the announcement spells it out: **"as before, each issuance will choose which intermediate to use at random, to discourage intermediate key pinning."** The most widely used CA on the internet has spent two years deliberately breaking that kind of pinning. This is not malice. It is hard-won experience, and the browser world went through it first. **HPKP** — pinning via an HTTP header, specified in 2015 by Google engineers — failed completely. Chrome deprecated it in version 67 and killed it in version 72, in January 2019. Google's stated reasons: very low adoption, risk of denial of service, and hostile pinning. The famous casualty is Smashing Magazine, October 2016: offline for four days for most of its readers. They rotated to a certificate with a new key, shipped a header containing only the new pin, and everyone holding a cached pin set was locked out. There was no rollback, because the old certificate had already expired. On the mobile side, Barclays, November 2016. Reportedly the app pinned an outdated intermediate, the chain changed, and payments stopped — on the eve of Black Friday. To fix it in time, Symantec issued a certificate under the old intermediate, and had to violate the CA/Browser Forum's serial-number requirement to do it. Same moral in both cases: **pinning does not break when you are attacked. It breaks on an ordinary Tuesday, when somebody renews a certificate.** ### Five rules if you are pinning anyway #### Rule 1. Pin the key, not the certificate Technically that is the SPKI — SubjectPublicKeyInfo, the structure inside the certificate that holds the public key. You pin the base64-encoded SHA-256 hash of it. Why this is the whole ballgame: **the certificate changes on every renewal, the key does not.** Pin the key and a same-key renewal never touches your pin. Pin the whole certificate fingerprint and it breaks every single time — twice a year at 200 days, eight times at 47. Here is the one-liner for a live host: ```bash openssl s_client -connect api.example.com:443 -servername api.example.com /dev/null \ | openssl x509 -pubkey -noout \ | openssl pkey -pubin -outform der \ | openssl dgst -sha256 -binary \ | openssl enc -base64 ``` And the same hash from a private key you have generated but not yet certified — which is how you compute a backup pin: ```bash openssl pkey -in backup.key -pubout -outform der \ | openssl dgst -sha256 -binary \ | openssl enc -base64 ``` The good news: both platforms natively support **only** SPKI, on Android and iOS both. So if you are pinning a whole certificate, you either hand-rolled it or you picked a library that does it for you — and almost certainly for no good reason. #### Rule 2. Backup pins are mandatory At least one key **entirely under your control.** That is Google's phrasing, and "entirely under your control" is the load-bearing part. Not "a second certificate from the same CA." A key pair you generated ahead of time and keep offline, one you can get a certificate for on demand. Its pin ships in the app alongside the live one and just sits there waiting. That missing backup pin is exactly what turned Smashing Magazine's bad day into four days of downtime. They still had the old key — their host had kept it. What they had shipped in the header was the fingerprint of the new one only. ```xml api.example.com YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg= sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis= ``` The iOS equivalent, which is declarative and enforced by the OS: ```xml NSAppTransportSecurity NSPinnedDomains api.example.com NSIncludesSubdomains NSPinnedLeafIdentities SPKI-SHA256-BASE64YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg= SPKI-SHA256-BASE64sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis= ``` That sample pins leaf keys. Apple's own documentation uses `NSPinnedCAIdentities` instead — the same structure one level up the chain, which is what rule 3 is about. #### Rule 3. Pick your level in the chain — and know what it costs There is no consensus here, so both positions follow. **Apple recommends pinning the CA rather than the server**, so that you can rotate server certificates without shipping an app update. **OWASP recommends the opposite**: pin the leaf, but always with a backup. | Level | Rotation cost | What you are trusting | Verdict in 2026 | | --- | --- | --- | --- | | Leaf | Free with a same-key renewal; a release otherwise | Exactly one key | Workable **only** with backup pins | | Intermediate | Unpredictable | Everything that intermediate issues | **Off the table** on public CAs | | Public root | Rare | Everything that CA will ever issue | So broad it is nearly pointless | | **Your own private root** | You decide | Your own PKI | The one clearly good case | Pinning an intermediate for publicly trusted certificates has been basically off the table since 2024: Let's Encrypt has six intermediates now and you cannot predict which one your certificate gets. If you are on a public CA, that leaves the leaf with backup pins, or the root. The configuration where pinning is still genuinely a good idea is **your own private CA**, one you control end to end. Pinning to your own root is sensible, predictable, and it does not break because somebody else changed their mind. Which, incidentally, is exactly what that Chromium engineer said in the quote at the top. #### Rule 4. Never ship straight to hard-fail The order is: **report-only mode** first — the pin gets checked, the connection does not get blocked, and misses go to telemetry. Watch for a week. Then hard-fail for one percent of users. Then a staged rollout to everybody. And here is the annoying part: **there is no built-in failure reporting on either platform.** Not in Android's Network Security Config, not in iOS App Transport Security. The one mechanism that ever did this was HPKP's `report-uri`, and it is dead. So you build the telemetry yourself: catch the exception, build an event — domain, key you got, keys you expected, app version — and send it **over a channel that is not pinned**, or the report about your outage will not make it out. One more thing: learn to tell an outage from an attack. **Everyone failing at once means you broke your own rotation.** Scattered failures mean a corporate proxy, an antivirus, or an actual interception attempt. #### Rule 5. Build the kill switch before you need it On Android there is one built in — the `expiration` attribute on the pin set. After that date, pinning simply stops: pins are not checked, traffic flows as if nothing had happened. A kill switch on a timer. But it has a price, and Google says so itself: **"setting an expiration time on pins may enable attackers to bypass your pinned certificates."** So it is both your insurance against bricking the app **and** a way to bypass your pinning by waiting. On a different page of the same documentation, Google recommends **"a sufficiently short expiration period."** Both statements are official and both are current. And here is our favourite detail in this whole story. **The canonical example in Google's documentation still carries `expiration="2018-01-01"`.** If you copied it — and everybody copies it — your pinning has been fully disabled for eight years. They updated that page in June 2026. The date stayed. On iOS there is no equivalent mechanism at all. Turning pinning off means shipping a release. If you are doing a kill switch over remote config or feature flags, watch out for the chicken-and-egg problem: the delivery channel cannot be protected by the same pins, or you can never recover. The right answer is to **sign the pin set with a key whose public half is baked into the app.** Then it does not matter what channel it arrived on. Add a monotonic version counter so nobody can replay an older, validly signed set. ### Flutter: where certificate pinning falls apart Everything above applies to everyone. This is the part we wrote the article for — we build Flutter apps, and this is the part we have had our hands in. **Here is the headline, and it goes against most people's intuition: Dart does not use the platform networking stack.** `HttpClient` from `dart:io` does not go through `OkHttp` or `HttpURLConnection` on Android, or `NSURLSession` on iOS. It opens a socket and runs the TLS handshake itself, through a copy of BoringSSL compiled into the Flutter engine. The practical consequence: **your `` in `network_security_config.xml` does nothing for Dart traffic.** Silently. No error, no warning — the request just goes through. This is not our discovery. It is [Flutter issue #96722](https://github.com/flutter/flutter/issues/96722), filed in January 2022 and still open. A member of the Flutter team reproduced it and wrote the finding down: on iOS the request is cancelled properly, on Android it goes through, and on a native Android control app carrying the same config it fails as it should. On iOS the behaviour is different: it is reported to work, because Dart on Apple platforms hands the trust decision to the system's `SecTrust`. That is not documented anywhere, and honestly it is the worst outcome. **Pinning that works on one platform and silently no-ops on the other is more dangerous than pinning that works nowhere** — because you tested it. On an iPhone. Then three traps. We have hit all three in production. **Dart gives you no access to the SPKI.** The `X509Certificate` class exposes `der`, `pem`, `sha1`, subject, issuer and validity dates. The public key is not among them. So virtually every tutorial pins a SHA-256 of the entire certificate — which, per rule 1, breaks on every renewal. Doing it properly means parsing ASN.1 by hand. **dio's own README shows two ways to get this wrong.** Here is the first, condensed from it: ```dart // dio's README example. Three problems in nine lines. dio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final client = HttpClient(context: SecurityContext(withTrustedRoots: false)); client.badCertificateCallback = (cert, host, port) => true; return client; }, validateCertificate: (cert, host, port) => cert != null && fingerprint == sha256.convert(cert.der).toString(), ); ``` `withTrustedRoots: false` plus a blanket `badCertificateCallback` disables chain building, expiry and hostname verification. The fingerprint it compares is a hash of `cert.der` — the whole certificate, which rule 1 says breaks on every renewal. And `validateCertificate` runs later than you think; see below. The README's second variant is worse: it pins inside `badCertificateCallback` itself, comparing `cert.pem` to a stored PEM. By definition that callback fires **only when validation has already failed.** If an attacker presents a certificate validly signed by any trusted CA, it never runs at all and nothing is ever compared. As a pinning mechanism it is broken by design. **The right hook runs too late.** dio does have one — `validateCertificate`, which fires on successful validation. But it runs **after the request has gone out and the response has already come back.** It is not just your request body and `Authorization` header on the wire: you have already accepted the response status and headers. The only thing that check saves is the response body. The request has already leaked. > **And then the packages** > > The most popular pinning package on pub.dev — around a hundred and sixty likes and close to fifty thousand downloads a month when we checked in August 2026 — uses a platform channel bridge to make a **separate native request** to your host, checks the fingerprint on that, and if it matches, sends the real request over a **different connection** with no pinning on it at all. One connection gets validated; another carries your data. Also, every request is now two requests. We are not naming it, because the point is not one maintainer: read the source of whichever pinning package you are using, and find out which connection it actually checks. **What to actually do.** On Apple platforms there is a clean answer: move your HTTP layer onto `cupertino_http`. That is a real `NSURLSession`, which means pinning is declarative — `NSPinnedDomains` in `Info.plist`, exactly as in rule 2. OS-enforced, SPKI-based, zero third-party code. On Android there is no equivalent. Cronet has its own pinning API, but the `cronet_http` package **does not export it**. The JNI binding is right there in the package. What the package does export is `enablePublicKeyPinningBypassForLocalTrustAnchors` — the switch that bypasses pinning, not the pinning. There is also one question nobody on the internet answers clearly, and we would rather flag it than pretend otherwise: whether Android's Network Security Config applies to traffic going through Cronet. Sources contradict each other and there is no official documentation either way. Test it yourself before you rely on either answer. Two closing warnings for Flutter specifically. **Third-party SDKs** — analytics, crash reporting, payments — make network calls through their own native clients, and your pinning does not touch them. The same split is what makes [deferred deep linking on Flutter](https://nerdy.pro/blog/deferred-deep-linking-app-clips-install-referrer) a per-platform problem rather than one implementation. And **if you migrate to `native_dio_adapter` to get the native networking stack, the dio-based pinning you copied from a blog post silently switches off**, because `SecurityContext` and both callbacks stop being invoked. ### How to check your pinning in half an hour Do this properly, because the naive version lies to you. The naive version is: stand up a proxy, install its certificate on the phone, see if the app breaks. It will break. And that means nothing — as covered above, since Android 7 apps do not trust user-installed certificates anyway. You will conclude that you are protected by pinning when the operating system was protecting you all along. #### The 30-minute pinning verification Run it once per platform. Anything unchecked is a finding. ##### Setup - Release build, not debug — a debug-overrides block invalidates the whole test - Proxy certificate installed in the SYSTEM trust store, not the user store - Flutter: traffic confirmed to reach the proxy via VPN capture or iptables, because Dart ignores system proxy settings - A real authenticated flow exercised, not just the first screen ##### What to look for - Your API traffic is unreadable — if you can read it, you have no working pinning - Third-party SDK traffic and your own traffic behave the same way, or you know why they do not - The failure you see is a pinning failure, not a trust-store failure - The same result on Android and on iOS ##### Then check the configuration itself - Pins are SPKI hashes, not certificate fingerprints - At least one backup pin, for an offline key you control - Android: the pin-set expiration date is in the future and deliberately chosen - A kill switch exists that does not require a store release - Failure telemetry is sent over a channel that is not itself pinned ### The short version Pinning is a narrow control against a threat that has become far less likely, with a blast radius covering your whole user base. It is genuinely necessary for finance, health and government. For everyone else, Google, Apple, Cloudflare and half of OWASP say don't, and they have reasons — the same reasons that killed HPKP and that are now cutting certificate lifetimes to 47 days. If you do it: the key, not the certificate. Backup pins with an offline key. Observation before hard-fail. Your own kill switch. And a clear view of what you are paying for your position in the chain. And if you are on Flutter, first find out whether it works at all. The three most likely answers: pinning disabled by a 2018 date copied out of Google's documentation; pinning declared in a configuration file Dart never reads; or pinning that validates a connection your data does not travel on. The blog-post version of this advice says "enable pinning, protect your users." The engineering reality is finding out what you are actually pinned to, verifying it fires on both platforms, and putting a spare key in a safe. Nobody makes viral videos about that. ### Sources Every claim above is checkable, so here is where each one comes from. - Google, "Security with HTTPS and SSL" — pinning "is not recommended for Android apps"; backup pins and "at least one key that's fully in your control": [developer.android.com/privacy-and-security/security-ssl](https://developer.android.com/privacy-and-security/security-ssl) - Google, "Network security configuration" — the `expiration` attribute, the bypass warning, "only `SHA-256` is supported", and the `2018-01-01` sample: [developer.android.com/privacy-and-security/security-config](https://developer.android.com/privacy-and-security/security-config) - Apple, "Identity Pinning: How to configure server certificates for your app" — "in most cases, pinning is not necessary and should be avoided", and the CA-over-leaf recommendation: [developer.apple.com/news](https://developer.apple.com/news/?id=g9ejcf8y) - Ryan Sleevi on `net-dev@chromium.org`, March 2021 — the private-CA quote in full: [groups.google.com](https://groups.google.com/a/chromium.org/g/net-dev/c/q_oSyjtMhts) - OWASP Pinning Cheat Sheet — "The answer to this is probably never", and the do-not-pin list: [cheatsheetseries.owasp.org](https://cheatsheetseries.owasp.org/cheatsheets/Pinning_Cheat_Sheet.html) - OWASP MASTG, network communication — "If an app does not implement pinning, this shouldn't be reported as a vulnerability. However, if the app must verify against MAS-L2 it must be implemented": [github.com/OWASP/mastg](https://github.com/OWASP/mastg/blob/master/Document/0x04f-Testing-Network-Communication.md) - CA/Browser Forum ballot SC-081v3 — the 398 → 200 → 100 → 47 schedule, filed by Apple, passed 11 April 2025: [cabforum.org](https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/) - Let's Encrypt, "Generation Y" — six intermediates, chosen at random "to discourage intermediate key pinning": [letsencrypt.org](https://letsencrypt.org/2025/11/24/gen-y-hierarchy/) - Cloudflare — the twelve unauthorized 1.1.1.1 certificates, and Fina's absence from the mobile root stores: [blog.cloudflare.com](https://blog.cloudflare.com/unauthorized-issuance-of-certificates-for-1-1-1-1) - Flutter issue #96722 — open since January 2022: [github.com/flutter/flutter](https://github.com/flutter/flutter/issues/96722) - dio, `IOHttpClientAdapter` — where `validateCertificate` actually runs, in source: [github.com/cfug/dio](https://github.com/cfug/dio/blob/main/dio/lib/src/adapters/io_adapter.dart) - Dart `X509Certificate` — the full list of what it exposes, with no public key on it: [api.dart.dev](https://api.dart.dev/stable/dart-io/X509Certificate-class.html) - `cronet_http`, `CronetEngine.build` — the parameter list, bypass included and pinning absent: [pub.dev](https://pub.dev/documentation/cronet_http/latest/cronet_http/CronetEngine/build.html) #### Should I use certificate pinning in my mobile app? Probably not, unless you are in finance, health or government services, or your threat model explicitly assumes a hostile network and an untrusted device. Google calls pinning not recommended for Android apps, Apple says it is unnecessary and should be avoided in most cases, and the OWASP Pinning Cheat Sheet answers should I pin with probably never. Do not pin if you cannot update the pin set without an app release, cannot know the key pair before production, or do not control both the server and the app. #### Is missing SSL pinning a real vulnerability? Not on its own. The OWASP Mobile Application Security Testing Guide states that an app not implementing pinning should not be reported as a vulnerability, and MASVS requires it only for apps that must verify against MAS-L2, the defence-in-depth profile for software handling sensitive data. A pentest report flagging missing SSL pinning as a finding is usually the output of an automated checklist rather than a judgement about your threat model. #### What should I pin — the certificate, the public key, or the CA? The public key, as an SPKI SHA-256 hash. The certificate changes on every renewal; the key does not, so a same-key renewal never touches your pin. Both Android and iOS natively support only SPKI pinning. For the level in the chain: pinning an intermediate is off the table for public CAs since the largest public CA began choosing intermediates at random in 2024, a public root is so broad it is nearly pointless, and the leaf works only with backup pins. Your own private root is the one clearly good case. #### Does certificate pinning work in Flutter? Not the way most people configure it. Dart does not use the platform networking stack: dart:io runs TLS through BoringSSL compiled into the Flutter engine, so a pin-set in network_security_config.xml is never consulted for Dart traffic on Android. That is Flutter issue 96722, filed in January 2022 and still open. On iOS it is reported to work because Dart hands the trust decision to SecTrust, which means the common outcome is pinning that holds on one platform and silently does nothing on the other. #### Why is badCertificateCallback the wrong place to pin in Dart? Because it fires only after validation has already failed. If an attacker presents a certificate validly signed by any trusted CA, the callback never runs and no fingerprint is ever compared. The commonly copied example that pairs it with SecurityContext withTrustedRoots false also disables chain building, expiry and hostname verification. The validateCertificate hook in dio is closer to correct, but it runs after the request has been sent and the response received, so it protects only the response body. #### How do the new 47-day certificate lifetimes affect pinning? They make leaf pinning that requires an app release untenable. The CA/Browser Forum ballot passed in April 2025 steps maximum TLS certificate lifetimes down from 398 days to 200 in March 2026, 100 in March 2027 and 47 in March 2029. Forty-seven days is about eight rotations a year. If your pin set can only change with a store release, that is eight forced releases a year, each subject to review and to users who do not update. Pinning the key rather than the certificate removes most of that cost, because a same-key renewal does not change the pin. #### How do I test whether my pinning actually works? Install your proxy certificate into the system trust store, not the user store, using an emulator with a writable system partition or Magisk, then read the captured traffic from a release build. The naive test — a user-installed certificate — proves nothing, because Android apps have not trusted user-installed certificates since targetSdk 24. On Flutter, separately confirm the traffic reaches the proxy at all, since Dart ignores system proxy settings. Then run the same test on the other platform and compare. If you want the background on what a certificate authority actually vouches for, we wrote that up in [HTTPS made simple](https://nerdy.pro/blog/https-free-ssl-certificate), and the cryptography underneath it in [how encryption works](https://nerdy.pro/blog/encryption-explained). This article is the companion to our video episode on the same subject; the previous one was [everyone got the timeline wrong](https://nerdy.pro/blog/everyone-got-the-timeline-wrong). And if you found something surprising when you ran the test — particularly the one-platform failure — we would genuinely like to hear it. Our bet is that half of all Flutter apps have pinning that fails on at least one platform. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that builds and maintains apps across fintech, healthcare, and retail. We also do [Flutter app development](https://nerdy.pro/services/flutter-app-development) for teams who would rather not find this out in production.* ## Everyone Got the Timeline Wrong: AI, Juniors, and Quantum https://nerdy.pro/blog/everyone-got-the-timeline-wrong **TL;DR.** Two years ago the loudest voices in tech told you that by now, software engineers would be a historical curiosity. Prompt in, product out. It is August 2026, and developer employment did not collapse. But the same wave that failed to remove developers removed something else, and almost nobody is telling that half of the story properly: it deleted the routine work that turned beginners into seniors. Employment of software developers aged 22 to 25 is down roughly 20% since late 2022, while older engineers in the same occupations held flat or grew. Quantum computing is now running the identical hype cycle one lap behind — with a genuinely historic milestone that nobody outside the field is discussing, and a fake one that is everywhere. ### Two things that are both true If you graduated with a computer science degree this year, you are competing with hundreds of other applicants for a listing that says *junior* and then quietly asks for three years of experience. And the profession as a whole is fine. Both of those things are true at the same time. Most takes pick one and pretend the other does not exist, which is why the discourse has been so useless. The interesting question is not whether AI took the jobs — it demonstrably did not — but why the damage landed so precisely on the people trying to get in. ### Why AI hasn't taken developer jobs Start with what is true, because the AI hype is not nothing. AI assistance is no longer a fringe workflow; it is the default one, and a large share of new code is written with a model in the loop. So why didn't the jobs disappear? #### Writing code was never the bottleneck Think about what you actually do in a working week. How much of it is typing new code? Maybe twenty percent, on a good week. The rest is figuring out what to build, understanding a system somebody else wrote five years ago, arguing about tradeoffs, and being the person who is *accountable* when it breaks at 3 AM. BCG put this well in their 2026 analysis: AI can dramatically accelerate code generation and testing, but it cannot own an outcome end to end. And you cannot cleanly split the work into "the model does the code" and "the engineer does the judgment," because in real software those two things are the same activity. The judgment is expressed *as* the code. #### The verification tax This is the counterintuitive one, and it is the finding we would point at first. METR — Model Evaluation and Threat Research, a nonprofit that runs studies on what these models can actually do — ran a controlled trial in which experienced open-source developers worked on their *own* codebases, half the tasks with AI tools and half without. They were 19% slower with AI. The part that should worry you is not the number. It is that they believed they had been faster. They estimated a speedup that did not exist. That is the shape of the whole problem. Time saved generating is paid back in review, debugging, and maintenance. It does not disappear — it moves, and it moves somewhere harder to see. We spend a lot of our week inside codebases where that bill came due; it is most of what an [AI code audit](https://nerdy.pro/services/ai-code-audit) actually finds. > **Speed you can feel is not speed you can measure** > > The METR result is uncomfortable precisely because the developers were experienced, working in code they knew, and sincerely reported the opposite of what the stopwatch said. Perceived velocity and actual velocity came apart. Any team deciding its AI strategy from how fast the work *feels* is reading an instrument that is known to be broken. #### Cheaper software means more software Every backlog at every company is a graveyard of things nobody had time to build. Make building cheaper and you do not fire the builders — you finally start clearing the graveyard. That has been the story of every tooling revolution since the compiler, and there is no particular reason this one inverts it. #### A lot of "AI layoffs" were not AI layoffs Companies cut for financial reasons and cite AI, because "we are leading the AI transformation" reads better to shareholders than "we over-hired in 2021 and then interest rates went up." There is a term for it now: AI washing. The tell is in the reversals — organizations that cut loudly on an AI narrative and then quietly restarted entry-level hiring once the balance sheet recovered. So: AI did not remove developers. It removed a *layer of tasks*. Hold onto that sentence, because that layer was the training ground. ### The junior developer squeeze Here is the number that matters. Stanford's Digital Economy Lab, working with payroll records from ADP — Automatic Data Processing, one of the largest payroll processors in the United States — across millions of workers, found that employment of software developers aged 22 to 25 is down roughly 20% since late 2022. Now look one row down in the same dataset. Across *all* AI-exposed occupations, employment fell 0.2% year over year. Older workers in those same occupations were flat or growing. The damage is not spread across the profession. It is concentrated almost entirely on the people trying to get in. #### Why it happened is not really about AI being smart The traditional path into this industry was an apprenticeship in disguise. You got hired, and for a year you did the work nobody senior wanted: fixing small bugs, writing tests, updating docs, building the boring CRUD screen. You were not profitable. Everyone knew you were not profitable. That was fine, because in twelve months you would know the codebase and become useful. AI ate that exact work. Not the interesting work. The *training* work. So companies now face a question they have not answered: how do you onboard a junior when there is nothing routine left to hand them? #### It compounds in three ways **The postings lie.** Listings labeled "entry-level" have grown while actual hiring into entry-level dropped — companies advertise a junior role and fill it with someone who has five years of experience. **You are not competing with other juniors.** The 2024–2025 layoffs pushed a large population of mid-level engineers into the market, and plenty of them will take a role labeled junior. **Carrying a junior got more expensive.** Seniors report spending meaningfully more time on review when juniors lean hard on AI assistants. The cost of a junior went up at the exact moment their obvious value went down. That is the cruel one. #### What to actually do about it Stop optimizing for the thing that got automated. Nobody is hiring you in 2026 because you can produce a React component — the model does that for free. The hiring case is the work the model demonstrably cannot do: - Read a large unfamiliar codebase and explain *why* it is shaped the way it is - Debug something in production when the logs are lying to you - Recognize when the AI output is confidently wrong — this is now the differentiator, and it is the skill juniors are weakest at - Own an outcome rather than a ticket And be strategic about *where* you aim. The open doors are not the ones on your feed. Enterprise, healthcare tech, fintech, government, defense, mid-size SaaS, small businesses — places with old codebases, compliance requirements, and real pain. Candidates who did an internship report markedly better offer rates than those who did not, and that gap is worth more than any framework you could learn this month. Hard is not the same as closed. But you have to read the market you are in, and most people applying right now are still applying to 2019. ### Quantum computing: what to actually expect The same hype machine is already warming up for round two, so it is worth being inoculated in advance. #### The result that actually matters Google's Willow chip demonstrated below-threshold error correction. In plain terms: for decades, adding qubits added more errors than you could correct, so scaling made things worse. Willow showed the opposite — add physical qubits, and the logical error rate goes *down*, exponentially. That single result moves quantum computing out of physics and into engineering. Engineering problems get solved. IBM is on a public roadmap toward a fault-tolerant machine, Starling, by 2029, with its current Nighthawk processor at 120 qubits running circuits with thousands of entangling gates and a target of demonstrating verified quantum advantage by the end of this year — notably backed by an open tracker so third parties can attack the claims. Microsoft and Quantinuum have logical qubits running with error rates better than the underlying hardware. Real progress, not vapour. #### What it does not mean **It does not mean a quantum computer replaces your laptop.** Quantum machines are not "faster computers." They are a specialized accelerator that beats classical hardware on a narrow set of problems: simulating quantum systems — chemistry and materials science — plus certain optimization and sampling problems. Your web app will never run on one. There is no quantum advantage for a REST API. **It does not mean encryption breaks next year.** Breaking RSA-2048 — Rivest–Shamir–Adleman, the public-key algorithm most of the internet runs on, at a 2048-bit key length — needs thousands of *logical* qubits, which at today's error rates means millions of physical ones. Current systems have hundreds of physical qubits. That is not a gap you close in a good quarter. The Global Risk Institute's expert survey puts the odds of a cryptographically relevant machine within ten years at roughly 17 to 22%. #### The one part that reaches your job "Harvest now, decrypt later." An adversary can record encrypted traffic today and decrypt it in 2035. So for anything with a long confidentiality lifetime — health records, state secrets, legal archives — the migration deadline is *now*, not when the machine exists. We walked through the mechanics of that threat — Shor's algorithm, what it does to RSA and ECC, and why agencies are already storing traffic they cannot yet read — in [our explainer on encryption](https://nerdy.pro/blog/encryption-explained). NIST, the US National Institute of Standards and Technology, has finalized the post-quantum cryptography standards, US federal agencies are working toward a 2030-ish target, and the practical work is unglamorous: inventory where your crypto lives, and move to hybrid key exchange. If you are still auditing where TLS terminates across your estate, that inventory is the same piece of work. That is the quantum action item for a normal developer in 2026. You do not need to learn Qiskit to keep your job. **And when you read the next headline, watch the right metric.** Physical qubit count is marketing. Logical qubit count, error rate, and circuit depth are engineering. Treat every "quantum advantage" claim as provisional until classical researchers have spent six months trying to beat it on a laptop — historically, a lot of them succeed. ### What about AI running on quantum computers? It is the least real thing in this entire discussion, and the arrow points the other way round. **The input problem.** To run a quantum algorithm on ordinary data, you first have to load that data into a quantum state, and for generic data the loading can cost as much as the computation. Training a language model is overwhelmingly about moving data around. Quantum is worst at exactly the thing that dominates the job. **Training does not scale.** There is a phenomenon called barren plateaus: as you add qubits to a deep parameterized circuit, the gradients shrink exponentially. The loss landscape goes flat and there is nothing left to descend. That is not a footnote — it is the central open problem of the field. **Dequantization.** Since Ewin Tang's 2018 result — she was an undergraduate, and she killed a famous quantum speedup by writing a classical algorithm that matched it — researchers keep finding the same thing. Give a classical algorithm the same data access the quantum version quietly assumes, and the advantage evaporates. Dequantization did not kill quantum machine learning. It drew the boundary around it. What is inside that boundary is real but narrow: data that is quantum to begin with, plus a handful of kernel methods with a proven separation. Nothing that looks like a language model. And here is the part we find genuinely funny. In April, NVIDIA shipped a family of open models called Ising. What do they do? Calibrate quantum processors and decode quantum error correction — substantially faster and more accurate than the traditional approaches. Read that again. AI is being used to build quantum computers, not the other way round. Gartner's near-term line is blunt: no enterprise AI workload running at scale on quantum hardware through 2028, and even fault-tolerant systems will not have the logical qubits for economically viable end-to-end AI by 2030. One warning: "quantum-inspired" is a marketing term for a classical algorithm running on classical hardware. When a vendor claims twenty-times-faster AI training from quantum optimization, check whether a single qubit was involved. Usually not one. ### The pattern The AI hype was wrong about developers being obsolete — and almost nobody noticed it was right about something worse and quieter, which is that we broke the ladder that turns beginners into seniors. We solved the training-work problem by deleting the training work. Quantum is running the same play. The real milestone — error correction below threshold — is historic, and almost nobody outside the field is talking about it. The fake milestone — encryption dies next year — is everywhere. Learn to tell those two apart and you will be right about technology more often than most people who work in it. #### Is AI actually reducing the number of software developers? Not in aggregate. Developer employment did not collapse, and older cohorts in AI-exposed occupations have been flat or growing. What changed is the distribution: employment of developers aged 22 to 25 is down roughly 20% since late 2022, per Stanford Digital Economy Lab research using ADP payroll records. AI removed a layer of routine tasks rather than removing developers, and that layer happened to be where juniors learned the job. #### Does AI make developers faster? Less reliably than it feels. In a controlled trial by METR, experienced open-source developers working in their own codebases were 19% slower on tasks where they used AI tools — and they estimated that they had been faster. Generation time gets paid back in review, debugging, and maintenance. The productivity is real in some contexts, but perceived speedup is not evidence of it, which is why teams should measure outcomes rather than trust the feeling. #### What should a junior developer focus on in 2026? The work a model cannot do. Reading a large unfamiliar codebase and explaining why it is shaped the way it is, debugging production when the logs mislead you, recognizing when AI output is confidently wrong, and owning an outcome rather than a ticket. Aim at enterprise, healthcare, fintech, government, and mid-size SaaS companies, where old codebases and compliance requirements create real demand. An internship remains worth more than any framework you could learn this month. #### Will quantum computers break encryption soon? Not soon, but the migration is urgent anyway. Breaking RSA-2048 needs thousands of logical qubits, which at current error rates implies millions of physical ones, against the hundreds that exist today. Expert surveys put a cryptographically relevant machine within ten years at roughly 17 to 22%. The urgency comes from harvest-now-decrypt-later: traffic recorded today can be decrypted later, so anything with a long confidentiality lifetime needs post-quantum migration now. NIST has finalized the standards. #### Will quantum computing make AI dramatically better? There is no credible near-term path. Loading ordinary data into a quantum state can cost as much as the computation, gradients vanish exponentially in deep parameterized circuits (barren plateaus), and dequantization results repeatedly show classical algorithms matching claimed quantum speedups once given the same data access. Gartner expects no enterprise AI workload running at scale on quantum hardware through 2028. Notably, the traffic runs the other way: AI is being used to calibrate and error-correct quantum processors. #### How can I tell a real quantum milestone from marketing? Watch the metric. Physical qubit count is marketing; logical qubit count, error rate, and circuit depth are engineering. Below-threshold error correction, where adding physical qubits lowers the logical error rate, is the genuinely historic result. Treat any quantum advantage claim as provisional until classical researchers have spent months trying to beat it on ordinary hardware, because historically many of them succeed. And quantum-inspired means classical: no qubits involved. If you are running a codebase where a lot of the code arrived faster than anyone could review it, that is the exact situation the verification tax describes — and it is what an [AI code audit](https://nerdy.pro/services/ai-code-audit) is for. We also wrote up [how we build with AI](https://nerdy.pro/blog/building-with-ai) and [the eleven problems we find in almost every AI-built codebase](https://nerdy.pro/blog/ai-code-audit-findings). And for anyone trying to get in right now: the ladder is broken, but the building is not. Aim at the work a model cannot do, and at the companies whose problems are old, unglamorous and real. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that builds and maintains apps across fintech, healthcare, and retail.* ## dxpdf 0.5.0: Teaching a DOCX Converter to Read the Rest of the World https://nerdy.pro/blog/dxpdf-0-5-0-release [dxpdf](https://nerdy.pro/open-source/dxpdf) turns `.docx` files into PDFs. No Microsoft Office, no headless LibreOffice, no cloud API — one Rust binary that parses the OOXML directly and paints the result through Google's Skia. We built it because the alternatives all made us choose between fidelity, speed, and not shipping customer documents to a third party — and because the one we actually ran, headless LibreOffice, turned out to be a desktop application we were babysitting in production. [Version 0.5.0](https://github.com/nerdy-pro/dxpdf/releases/tag/v0.5.0) shipped on 11 August. It is the release where the converter stopped assuming, in a dozen small places, that the document it was handed is written in English. ### Key takeaways - **0.5.0 is an internationalization release.** UAX #14 line breaking (including Thai, Lao, Khmer and Burmese), UAX #9 bidirectional text with rule L4 mirroring, and CLDR-driven numbers and dates that follow the document's own `w:lang`. - **The first outside contribution landed** — [@ikashapov](https://github.com/ikashapov) added Russian numbering formats, comment-reference parsing, and three list-label fixes. - **Locale data ships as one trimmed blob**, loaded through `icu_provider_blob` rather than each ICU4X crate's built-in `compiled_data`, because "correct in every locale" should not mean "and the binary is now enormous." - **Font resolution, not document size, decides what a conversion costs** — a 9-page, 1.3 MB document converts in 55 ms while a 3-page, 34 KB one takes 170 ms, and the entire difference is how their fonts resolve. - **Coverage against ISO 29500 is now 74 features fully implemented, 11 partial, 12 not yet supported** — and that last column is the most honest roadmap we can publish. - **It replaced a headless LibreOffice pipeline** — a desktop suite behind a queue, a watchdog, and a profile directory per worker. What that cost is below. - dxpdf is [MIT-licensed and open source](https://github.com/nerdy-pro/dxpdf), on [crates.io](https://crates.io/crates/dxpdf) and [PyPI](https://pypi.org/project/dxpdf/), and it now ships a `.deb` too. ### Thank you to our first outside contributor 0.5.0 is the first dxpdf release with a **New Contributors** section, and it is a good one to start with. [@ikashapov](https://github.com/ikashapov) landed [#118](https://github.com/nerdy-pro/dxpdf/pull/118): parsing for `w:commentReference`, the `russianUpper` and `russianLower` numbering formats, and fixes for three separate list-label layout and numbering defects. That is not a drive-by typo fix. Numbering formats are exactly the kind of feature that only gets found by someone whose documents actually use them — a Russian-language contract with an `а)`/`б)`/`в)` list is not something that turns up in a fixture set assembled in English, and the three label defects it uncovered were real bugs sitting in the layout path for anyone else with the same document shape. Thank you. If you are reading this and dxpdf mangles something in *your* documents, that is the contribution we want most. More on that at the end. ### What we ran before: headless LibreOffice Before dxpdf there was `soffice --headless --convert-to pdf`, wrapped in a queue and a retry, like most document pipelines. It works, and for a long time it was the right call — nothing else converts DOCX with that much fidelity for the price of an `apt install`. What it is not is a component you can put in a request path and stop thinking about: - **It is a desktop application in server costume.** There is no library API and no in-process call: you launch a binary, read an exit code, and hope — from the outside, a failed conversion and a crashed process look much the same. The first conversion in a fresh process also pays for starting an entire office suite. - **One process, one profile.** `soffice` serializes around its user profile directory, so every concurrent worker needs its own or they contend on the same lock files. Scaling out is process management, not a thread pool. - **It hangs.** An unusual or malformed document can leave `soffice` waiting forever, so a production setup grows a timeout, a watchdog, and a reaper for orphaned processes. Every team that runs this eventually writes the same babysitting code. - **Memory and image size.** Hundreds of megabytes resident per instance, and a container image carrying a full office suite — plus the fonts, which have to be installed or the metrics silently substitute and the layout moves. - **Fidelity drifts with the version.** The same document converted on a different LibreOffice release can paginate differently, which turns a base-image bump into a change in what the customer receives. Finding that out from the customer is the bad way to find it out. None of this is a bug report against LibreOffice. It is an office suite and it is very good at being one; this is a statement about what happens when a GUI application becomes the load-bearing part of a server-side pipeline. dxpdf came out of wanting one library call, a predictable memory profile, and output that changes only when we change it. ### The problem: a layout engine that only knew one alphabet The premise of dxpdf has always been fidelity. A DOCX-to-PDF converter is only useful if the page breaks land where Word puts them — an invoice whose total row slides onto page two is worse than no PDF at all. Everything in the project's [architecture](https://github.com/nerdy-pro/dxpdf#architecture) exists to serve that: parse the OOXML into an immutable model, resolve the style cascade, lay out and measure before painting anything. What we had built by 0.4.0 was a layout engine that did this very well *for text made of spaces and Latin letters*. Every assumption underneath was invisible until it broke: - **Lines were broken at whitespace.** That is not a line-breaking algorithm, it is a heuristic that happens to work for English. Thai, Lao, Khmer and Burmese are written without spaces between words — under a whitespace rule, a Thai paragraph is one unbreakable token that runs off the edge of the page. - **Text was laid out left to right, always.** An Arabic or Hebrew paragraph came out in visual nonsense: correct glyphs, wrong order, unmirrored brackets. - **Numbers and dates were formatted the American way.** A decimal tab stop aligned on `.` when a German document meant `,`. A `DATE` field with a localized picture string — `TT.MM.JJJJ` rather than `dd.MM.yyyy` — was not understood at all. - **Spelled-out list numbering only knew English.** `cardinalText` in a German document produced `One`, not `Eins`. - **Character spacing operated on code points.** Add 2 pt of tracking to a string with a combining mark and the mark drifts away from the letter it belongs to. None of these are exotic. They are what happens the first time a converter written by English speakers meets a document that was not. ### The solution: real specifications, not more heuristics The through-line of 0.5.0 is that each of those got replaced with the actual Unicode or OOXML algorithm rather than a better guess. **Line breaking is now UAX #14**, via ICU4X, resolved per *paragraph* rather than per run — so a word split across `` boundaries by a stray formatting change still breaks where the algorithm says, not where the XML happens to be cut. The four scripts UAX #14 explicitly hands off to "complex context analysis" get LSTM-driven word boundaries. A token that no rule permits breaking is cut at the container edge instead of overflowing it, which is what Word does and what a narrow table cell needs. **Bidirectional text is now UAX #9** — embedding levels resolved per paragraph, reordered per line, with rule L4 mirroring so parentheses and brackets face the right way. `w:jc` alignment and `w:ind` indentation resolve against the paragraph's base direction rather than against "left." **Scripts with positional forms are shaped through HarfBuzz.** Arabic, Syriac, N'Ko, Mongolian, Adlam and friends need cursive joining to be legible at all, so a run whose script has positional forms goes through Skia's HarfBuzz; everything else keeps the existing, cheaper cmap path. **Numbers and dates follow `w:lang`.** Decimal separators come from CLDR and are region-aware — `de-CH` and `de-DE` disagree, and now dxpdf disagrees with them correctly. `DATE` and `TIME` fields evaluate with localized §17.16.4.2 picture names. Numbers spell out in English, German, French and Spanish (`Eins`, `Vingt et un`, `Veintiuno`, `1.º`), with digits as the fallback for everything else. **Character spacing and justification operate on grapheme clusters**, per UAX #29 — so §17.3.2.35 tracking never separates a combining mark from its base, and §17.3.1.13 `distribute` alignment shares spare width *between* clusters rather than inside one. Alongside the i18n work, 0.5.0 also fixed the pagination edges that get noticed immediately: a continuous section break is promoted to a page break when the page setup actually differs, a paragraph marked keep-with-next stays at the foot of its page before an explicit break, and break-only paragraphs get the line height they are supposed to contribute. Plus font synthesis for bold and oblique when a face has no real bold, [Windows in the CI matrix](https://github.com/nerdy-pro/dxpdf/pull/123), and Debian packaging. ### Three decisions worth explaining #### Units are types, not numbers OOXML measures things in twips, EMUs, half-points, eighth-points and thousandths of a percent, sometimes three of them in the same element. The obvious approach is to convert everything to `f64` at the parser boundary and get on with it. dxpdf does the opposite: each OOXML unit is its own `i64`-backed type in `model::dimension`, they flow through parse and resolve unconverted so they round-trip losslessly, layout works exclusively in `Pt`, and raw `f32` appears only at the Skia boundary. ```text DOCX (ZIP) → Parse → Document Model → Resolve → Layout → Subset → Paint → PDF Twips/Emu/HalfPoints ←──── Pt throughout ────→ Skia ``` The payoff is that adding a twip value to a half-point value is a compile error rather than a document that is off by a factor of ten in one margin. This class of bug is miserable to find by eye — the output still looks like a plausible document, just subtly wrong — and it disappears entirely if the compiler refuses to let the units mix. Geometry types are generic over the unit for the same reason, with the `Pt`-specialized equivalents kept in `render::geometry` so the model layer carries no Skia dependency at all. #### Locale data is one trimmed blob, not compiled-in defaults ICU4X crates ship with a `compiled_data` feature that bakes the full CLDR dataset into the binary. It is the easy path and it is enormous — you get every locale, every calendar, every currency, whether the document names them or not. dxpdf instead builds a single trimmed data blob covering the locales it actually supports and loads it through [`icu_provider_blob`](https://crates.io/crates/icu_provider_blob). More build infrastructure, one artifact to keep in sync, and a binary a CLI user will actually install. The same instinct shows up next door in the shaping decision: [`unicode-joining-type`](https://crates.io/crates/unicode-joining-type) is used as a *predicate* — the thing whose only job is to keep HarfBuzz off Latin text — so the expensive path runs for the scripts that need it and nothing else. Both are the same trade: pay for correctness where it is required, not everywhere uniformly. #### Layout got a speculative scope Section clearance (§17.6.22) cannot be decided going forward. Whether a continuous section break stays on the current page depends on what comes after it, so layout has to try a placement, look ahead, and undo the attempt if the answer comes back wrong. That is impossible if your layout state is a `&mut` you have been mutating all the way down. [#111](https://github.com/nerdy-pro/dxpdf/pull/111) added a speculative `BuildState` scope: a region of layout work that can be committed or discarded wholesale. It is a good illustration of a pattern that recurs across this project — the specification does not just tell you what to implement, it tells you what shape your architecture has to be. Word's layout has look-ahead in it, so a converter that wants Word's page breaks needs somewhere to put a discarded attempt. ### The numbers Measured on an Apple M3 Max with `hyperfine` (30 runs, 5 warmup) at v0.5.0, against fixtures committed in the repo so the numbers are reproducible: | Fixture | Pages | Input | Conversion time | Peak RSS | | --- | --- | --- | --- | --- | | `sample-docx-files-sample3` | 3 | 34 KB | **170 ms** | 55 MB | | `sample-docx-files-sample-4` | 7 | 10 KB | **170 ms** | 52 MB | | `sample-docx-files-sample1` | 9 | 1.3 MB | **55 ms** | 42 MB | | `sample-docx-files-sample4` | 171 | 14 MB | **420 ms** | 159 MB | The interesting row is the third one. A 9-page document carrying forty times the input converts in a third of the time the 3-page one needs — because **font resolution, not document size, decides what a conversion costs**. The font registry is built in tiers and lazily: a document whose fonts are embedded or already on the host never reaches the expensive tier and spends about 4 ms there, while one that has to fall back to the host metadata index and match on PostScript and style names pays 120–185 ms, once. On the 3-page fixture that lookup is roughly five times parse, layout, subsetting and painting put together. For anyone sizing a batch workload, that reframes the question entirely. It is not *how big are the documents* — it is *do they name fonts the host already has*. Documents written by Word usually do. Coverage and adoption, at the time of writing: - **74 OOXML features fully implemented, 11 partial, 12 not yet supported**, validated against ISO 29500. The full matrix is in the [README](https://github.com/nerdy-pro/dxpdf#ooxml-feature-coverage), gaps included. - **~6,000 downloads on crates.io** across 38 published versions, plus PyPI wheels for macOS, Linux and Windows on Python 3.8+. - **29 stars, 6 forks**, MIT-licensed, five months old. - Used in production by [nerdy.pro](https://nerdy.pro) and [formtastic.de](https://formtastic.de). And the honest other side of that ledger: no Indic reordering, no per-glyph font fallback, no automatic hyphenation, no tracked changes or comments, no SmartArt or charts, and tight/through image wrapping approximated by the bounding box rather than a polygon. All of it is listed, with `❌` next to it, in the same table as the wins. ### Install it ```bash cargo install dxpdf # CLI pip install dxpdf # Python ``` ```bash curl -LO https://github.com/nerdy-pro/dxpdf/releases/download/v0.5.0/dxpdf_0.5.0-1_amd64.deb sudo apt install ./dxpdf_0.5.0-1_amd64.deb ``` - **Source and issues** — [github.com/nerdy-pro/dxpdf](https://github.com/nerdy-pro/dxpdf) - **Release notes** — [v0.5.0](https://github.com/nerdy-pro/dxpdf/releases/tag/v0.5.0) - **Crate** — [crates.io/crates/dxpdf](https://crates.io/crates/dxpdf) · **API docs** — [docs.rs/dxpdf](https://docs.rs/dxpdf) - **Python package** — [pypi.org/project/dxpdf](https://pypi.org/project/dxpdf/) - **Project page** — [dxpdf on nerdy.pro](https://nerdy.pro/open-source/dxpdf) - **Background reading** — [how we generate PDFs in Rust with Skia](https://nerdy.pro/blog/skia-rust-pdf-rendering), the foundation everything above is painted on ### Contributions are genuinely wanted dxpdf is written in [Rust](https://nerdy.pro/technologies/rust) and the contribution loop is short. Two things are worth more than they might look: **A DOCX that renders wrong is as valuable as a patch.** The project is fixture-driven — a document that reproduces a defect, committed alongside the fix, is how every feature in that coverage table got locked in. If dxpdf mangles a document you own, [open an issue](https://github.com/nerdy-pro/dxpdf/issues) with the file, or the smallest version of it you can share. **The `❌` column is the roadmap.** Automatic hyphenation, per-glyph font fallback, mirrored tab stops under `w:bidi`, `chineseCounting` and the other counting formats, SmartArt — each of those is a well-scoped piece of work with a spec section attached to it. @ikashapov's first PR started exactly there. Please open an issue before a large PR, and run what CI runs before you push: ```bash cargo fmt --all -- --check cargo clippy --all-targets -- -D warnings cargo test --all ``` Project conventions are in [`AGENTS.md`](https://github.com/nerdy-pro/dxpdf/blob/main/AGENTS.md). ### Frequently asked questions #### What is new in dxpdf 0.5.0? 0.5.0 is an internationalization release: UAX #14 line breaking via ICU4X (including Thai, Lao, Khmer and Burmese), UAX #9 bidirectional text with rule L4 mirroring, HarfBuzz shaping for cursive-joining scripts, region-aware decimal separators and localized DATE/TIME field pictures driven by the document's w:lang, and numbers spelled out in English, German, French and Spanish. It also fixes several pagination edge cases, adds font synthesis for bold and oblique, adds Windows to the CI matrix, and ships a .deb for Debian and Ubuntu. #### How does dxpdf compare to running headless LibreOffice? LibreOffice in --headless mode is the usual answer and it converts DOCX with good fidelity, but it is a desktop suite running server-side. It serializes on its user profile directory, so each concurrent worker needs its own; it can hang on unusual documents and needs a timeout, a watchdog and a reaper around it; it carries hundreds of megabytes of resident memory and a container image to match; and its output can change when the LibreOffice version changes. dxpdf is a single Rust binary with a library API, a predictable memory profile, and output that changes only when the converter does. #### Does dxpdf require Microsoft Office or LibreOffice? No. dxpdf is a standalone Rust binary that reads DOCX files directly and renders PDF output through Skia. There is no Office installation, no headless LibreOffice process, and no external service — which also means documents never leave the machine doing the conversion. #### How fast is dxpdf? On an Apple M3 Max the committed fixtures convert in 55–170 ms, and a 171-page, 14 MB document in about 420 ms. Document size matters less than font resolution: a document whose fonts are embedded or already present on the host spends around 4 ms resolving them, while one that falls back to the host metadata index pays 120–185 ms once. #### Which languages and scripts does dxpdf handle? As of 0.5.0: UAX #14 line breaking for all scripts including the space-less ones (Thai, Lao, Khmer, Burmese), UAX #9 bidirectional text for Arabic and Hebrew with mirroring, and HarfBuzz shaping for scripts with positional forms such as Arabic, Syriac, N'Ko, Mongolian and Adlam. Indic reordering is not yet supported, and there is no per-glyph font fallback — a document must name a font covering the characters it uses, as Word writes. #### Can I use dxpdf from Python? Yes. Install it with pip install dxpdf and call dxpdf.convert(bytes) or dxpdf.convert_file("input.docx", "output.pdf"). Wheels are published for macOS, Linux and Windows on Python 3.8 and newer, so there is no Rust toolchain required to use it. #### How can I contribute to dxpdf? Open an issue with a DOCX that renders incorrectly — the project is fixture-driven, so a reproducing document is as useful as a patch. For code, the unsupported entries in the README's feature matrix are the roadmap, each with an ISO 29500 section attached. Open an issue before a large PR, and run cargo fmt, cargo clippy and cargo test before pushing. ### Converting documents at scale? dxpdf is [MIT-licensed and open source](https://nerdy.pro/open-source/dxpdf) — use it, fork it, or tell us where it falls short on your documents. If you need document processing built into a product rather than bolted onto it, [get in touch](https://nerdy.pro/contact); this converter exists because we kept needing one for client work and would rather maintain it in the open than rebuild it privately every time. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that also builds and maintains the infrastructure tooling — like [dxpdf](https://nerdy.pro/open-source/dxpdf) and [Orosu](https://nerdy.pro/open-source/orosu-server) — that its own delivery work runs on.* ## Orosu: Why We Replaced SSH Deploy Keys With Signed WebSocket Jobs https://nerdy.pro/blog/orosu-secure-cicd-deployments Almost every project we ship eventually needs the same unglamorous thing: get a build artifact from CI/CD onto a server and run it. Nobody budgets time for this — it is "just deployment" — so it tends to get solved once, under deadline pressure, with whatever is fastest to type. Usually that is an SSH private key dropped into CI secrets and a shell script that SCPs a file over and restarts a service. It works. It also does not scale past the first project, and we kept rebuilding a slightly different version of it for every client until we finally sat down and wrote the tool we actually wanted: [Orosu](https://nerdy.pro/open-source/orosu-server). > From Japanese 降ろす (*órosu*) — "to unload" or "offload." Orosu is a small Rust service — `orosu-server` — that you install once on a machine. Instead of CI holding an SSH key that opens a shell, CI holds an Ed25519 key that can trigger exactly one of a fixed set of scripts the machine's owner defined ahead of time, over an authenticated WebSocket connection. This post is about why that distinction matters enough to build a whole tool around it, and what it looks like in practice. ### Key takeaways - **The problem isn't SSH itself — it's what SSH access implies.** An SSH key that can log in to run one deploy script can run anything. Every credential that reaches a production box inherits the full blast radius of a shell, whether the job needs it or not. - **Orosu narrows that down to a closed set of predefined scripts.** CI selects a script by name and passes arguments; it cannot supply an arbitrary command, no matter what the pipeline is compromised into sending. - **Authentication is a signature, not a secret on the wire.** Every job is signed with the client's Ed25519 key. There is no password or shared secret an attacker can intercept mid-connection. - **It is one Rust binary with no runtime dependencies** — installed via apt or a prebuilt binary from GitHub Releases, fronted by whatever reverse proxy already terminates TLS on the box. - **An optional end-to-end encryption layer** covers the one gap TLS-at-the-proxy leaves open: keeping script arguments and output confidential from that proxy itself. - Orosu is [Apache-2.0 licensed and open source](https://nerdy.pro/open-source/orosu-server) — we use it, and we would rather other teams stop reinventing this too. ### The shape of the problem Strip the branding off "deploy from CI" and it is always the same request: run this script, on that machine, with these files. The default way most teams satisfy that request accumulates risk in a specific, predictable pattern: - An **SSH private key** goes into CI secrets. It usually is not scoped to one script — it is scoped to a shell, because scoping SSH to "only this one command" is fiddly enough that almost nobody actually does it. - The same key, or a near-copy of it, gets **reused across every repository and pipeline** that needs to reach that server, because generating and rotating a distinct key per pipeline is more process than anyone wants to own. - The **deploy script itself gets copy-pasted** from the last project, small differences accumulate, and nobody remembers which version is authoritative. - **Production becomes directly reachable** from CI runners — a class of hosts an attacker specifically targets, because a compromised runner with SSH access is a compromised server. - The day a key *does* leak, rotating it means finding **every pipeline that might reference it**, because there was never a registry — just secrets sprawled across however many repos accumulated a copy. None of this is a skill problem. It is the default shape of "run a script on a remote machine from a pipeline" when the tool you reach for is a general-purpose remote shell. SSH is not wrong for logging in and debugging a box by hand. It is the wrong primitive for a CI job that only ever needs to trigger the same three or four deploy scripts. ### What we actually wanted Before writing any code, the requirements were less about cryptography and more about what a deploy credential should be *allowed* to do: 1. **A CI credential should not be able to open a shell.** It should be able to trigger a script, by name, from a list the server owner controls — full stop. 2. **Authentication should not depend on a secret that travels with every request.** A signature proves possession of a key without exposing it. 3. **Adding a new deploy target should not mean generating a new SSH key pair and threading it through server config by hand** — it should be a config entry and a `keygen` command. 4. **The server should be the source of truth for what can run**, not the pipeline. A compromised or careless CI job should never be able to expand its own permissions. That list is a WebSocket protocol, not an SSH replacement wearing a costume — which is exactly what Orosu turned into. ### How it works `orosu-server` runs on the target machine. It listens for WebSocket connections, verifies an Ed25519 signature on every incoming job, matches the job's script name against that client's configured allowlist, and — only if all of that checks out — runs the script with whatever arguments and attached files the job carried. ```yaml listen: tcp: "127.0.0.1:8081" clients: - name: my-ci-client secret_file: /etc/orosu/my-ci-client.pub scripts: - name: deploy command: - "bash" - "/etc/orosu/scripts/deploy.sh" ``` That `scripts:` list is the whole security model in one place. `my-ci-client` can trigger `deploy` and nothing else — not a different script on the same box, not an arbitrary shell command, not a script belonging to a different client. If the CI pipeline that holds this client's key is compromised, the attacker inherits exactly one deploy script's worth of capability, not a login. A client's key pair comes from `orosu-keygen`: ```shell orosu-keygen --name my-ci-client --private-key-output my-ci-client.key --public-key-output my-ci-client.pub ``` The public half goes into the server config above; the private half becomes a CI secret, used only to sign requests — it never grants a session by itself. Triggering the script from GitHub Actions is the companion Action: ```yaml - name: Deploy uses: orosu-ci/orosu@v0 with: address: ${{ secrets.OROSU_SERVER_URL }} script: deploy key: ${{ secrets.OROSU_CLIENT_KEY }} arguments: ${{ github.sha }} ``` That is the entire pipeline change: an SSH step becomes an Orosu step, and the server-side surface shrinks from "anything this key's shell can do" to "this one named script." `orosu-server` itself is typically bound to localhost and fronted by a reverse proxy that terminates TLS and forwards the WebSocket upgrade — nginx, in the common case, the same box that is probably already doing this for whatever the deploy script restarts. ### Why not just scope an SSH key with `command=` SSH does technically support a `command=` restriction in `authorized_keys`, and we get asked why Orosu is not just that. Two reasons it falls short in practice, both about what happens *around* the happy path rather than the happy path itself: - **It is per-key, hand-edited server config, invisible to the pipeline author.** Nobody enforces that every deploy key in `authorized_keys` actually carries a `command=` restriction — the file has no schema, and an unrestricted key sitting next to nine restricted ones looks identical at a glance. - **A restricted SSH session still speaks a shell.** Depending on the script and how `command=` interacts with the arguments SSH forwards, the boundary between "run this one thing" and "run this one thing, with injectable arguments" is easy to get subtly wrong. Orosu's script arguments are protocol-level fields, not shell tokens assembled from an SSH command line — there is no shell in the path for an argument to escape out of. Orosu is not claiming SSH is insecure. It is claiming that the safe version of "restrict SSH to one command" is enough extra ceremony that almost nobody does it consistently, and a tool whose *only* job is running one of a few predefined scripts can make that the default instead of an opt-in. ### Confidentiality past the reverse proxy WSS covers the wire, but TLS terminated at a reverse proxy in front of `orosu-server` — the standard setup above — means script arguments, uploaded files, and output are plaintext at that proxy. For most deploy scripts that is a non-issue; for some it is not (a deploy argument can be a credential the script forwards onward, a rollback token, a customer identifier). Orosu's answer is an optional end-to-end encryption layer — X25519 for key agreement, HKDF-SHA256 to derive session keys, ChaCha20-Poly1305 to encrypt — that sits on top of the WebSocket transport and is opt-in on both ends independently: ```shell orosu-keygen --kind server --private-key-output server.key --public-key-output server.pub ``` The server's public key goes into its config and into the GitHub Action's `server_key` input. A server with encryption configured still serves clients that omit `server_key` exactly as before — there is no coordinated cutover, no flag day, and no breakage for a client that has not upgraded yet. ### Security hardening, honestly We treat this as a first-class part of the tool, not a checkbox. A few specifics, because "we care about security" is a claim we would rather back with what actually changed: the 0.7.0 release added low-order-point checks on the X25519 exchange (closing a known class of curve-based attack), hardened attachment extraction so a crafted or oversized zip cannot path-traverse out of its directory or exhaust disk via decompression, and made `orosu-keygen` write private key files with `0600` permissions regardless of the umask in effect. All three shipped as a drop-in upgrade — same config, same protocol, same CLI. The full history is in [`CHANGELOG.md`](https://github.com/orosu-ci/server/blob/main/CHANGELOG.md). ### What Orosu is not Scope is a feature here, not a gap, but worth stating plainly. Orosu does not orchestrate rollouts, manage rollbacks, or understand your deployment topology — it runs a script you already wrote, and the script decides what a deploy means. It is not a Kubernetes-native tool; if your target is already a cluster with a real deployment controller, that controller is almost certainly the better answer and Orosu is solving a problem you no longer have. Where it earns its place is the machine that is *not* a cluster node — the single VPS, the bare-metal box, the "we will containerize this eventually" server that every agency and a lot of startups still run some part of their stack on. ### Getting started `orosu-server` and `orosu-keygen` ship as Debian/Ubuntu packages: ```shell curl -fsSL https://packages.nerdy.pro/NerdyPro.gpg | sudo gpg --dearmor -o /usr/share/keyrings/nerdy-pro.gpg echo "deb [signed-by=/usr/share/keyrings/nerdy-pro.gpg] https://packages.nerdy.pro/ stable main" | sudo tee /etc/apt/sources.list.d/nerdy-pro.list sudo apt update sudo apt install orosu ``` GitHub Releases also publish prebuilt binaries for machines that cannot add an apt source. The [full setup walkthrough](https://nerdy.pro/open-source/orosu-server) — generating keys, writing the server config, wiring up the GitHub Action — is on the project page. Orosu is written in [Rust](https://nerdy.pro/technologies/rust), the same language behind [dxpdf](https://nerdy.pro/open-source/dxpdf), our DOCX-to-PDF engine — it is where we reach when a tool has to be fast, predictable, and safe against input that is not fully trusted, like a client-attached file arriving over the wire. ### Frequently asked questions #### Why not just use restricted SSH keys for CI deployment? SSH's command= restriction in authorized_keys can scope a key to one command, but it is hand-edited, per-key server config with no schema — nothing stops an unrestricted key from sitting unnoticed next to correctly restricted ones. A restricted session also still speaks a shell, so argument handling has to be gotten right by hand. Orosu makes the script allowlist the default, protocol-level behavior rather than an opt-in convention, and passes arguments as protocol fields with no shell for them to escape out of. #### What happens if a CI pipeline using Orosu is compromised? The attacker inherits exactly the scripts that client's key is configured to trigger, with whatever arguments the protocol allows for that script — never an arbitrary shell command, and never another client's scripts. That is a materially smaller blast radius than a compromised SSH key, which typically inherits a full login shell. #### Does Orosu replace Kubernetes deployments? No. Orosu is for machines that are not already managed by a cluster controller — a VPS, a bare-metal box, a server running a mix of services outside Kubernetes. If your target already has a real deployment controller, that controller is the better tool. #### Is the connection to orosu-server encrypted? WSS/TLS covers the connection by default, typically terminated at a reverse proxy. An optional end-to-end layer (X25519 + HKDF-SHA256 + ChaCha20-Poly1305) additionally protects script arguments, files, and output from that proxy itself, and is opt-in independently on the client and server. #### Is Orosu open source? Yes, under Apache-2.0. The server, the orosu-keygen CLI, and the GitHub Action are all on GitHub, along with an apt repository and prebuilt release binaries. ### Deploying to a server you do not want to SSH into anymore? That is exactly what Orosu is for. Read the [full project documentation](https://nerdy.pro/open-source/orosu-server) for the complete setup, browse the rest of [our open-source work](https://nerdy.pro/open-source), or [get in touch](https://nerdy.pro/contact) if you want us to look at how your team ships to production — deploy pipelines built under deadline pressure are a specific, common finding in an [AI code audit](https://nerdy.pro/services/ai-code-audit), and Orosu is the fix we reach for as much as we recommend it. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that also builds and maintains the infrastructure tooling — like [Orosu](https://nerdy.pro/open-source/orosu-server) and [dxpdf](https://nerdy.pro/open-source/dxpdf) — that its own delivery work runs on.* ## Flutter App Maintenance Cost in 2026: What You'll Actually Pay After Launch https://nerdy.pro/blog/flutter-app-maintenance-cost **TL;DR.** - Budget roughly **15–25% of your Flutter app's initial build cost, per year**, for maintenance — a $30K app runs about $4.5–7.5K a year to keep healthy. - Maintenance breaks into four buckets: fixing bugs (**corrective**), keeping up with OS/SDK/API changes (**adaptive**), improving what already works (**perfective**), and paying down tech debt before it compounds (**preventive**). - Flutter is **one maintenance stream, not two** — one codebase to upgrade, test, and release, instead of separate iOS and Android teams doing that work twice. - A cheap build can still be an expensive app to own: rushed or unsupervised AI-generated code with no tests and no architecture inflates every one of the four buckets above. - Maintenance is a percentage that compounds every year the app stays alive — budget for it at launch, not after the first outage. ### What "app maintenance" actually covers "Maintenance" gets used as a catch-all for anything that happens after launch, which is exactly why it's easy to underbudget. This four-way split isn't something we made up to sound rigorous — it's the standard taxonomy in ISO/IEC 14764, the international standard for software maintenance, and it holds up in practice: - **Corrective** — fixing bugs that make it to production: crashes, incorrect calculations, broken flows. This is the maintenance people picture first, and it's usually the smallest bucket in a healthy codebase. - **Adaptive** — changes forced on you by the outside world: a new Flutter stable release, a new iOS or Android version, a store policy update, a payment provider changing its API. You didn't choose to do this work; the platform did. - **Perfective** — improvements nobody's forcing on you: performance tuning, UX polish, new features on existing screens. This is where "maintenance" quietly turns into "product development," and it's fine — it's still ongoing cost. - **Preventive** — refactoring, dependency upgrades, security patching, and tech-debt paydown done *before* something breaks, not after. This is the bucket that gets cut first when budgets are tight, and the one that's most expensive to have skipped two years later. A realistic annual maintenance budget has to cover all four, not just the corrective one most founders picture. ### What it costs **Plan for 15–25% of your initial build cost per year.** Where you land in that range depends on how actively the app ships new features (perfective work scales with product velocity), how many third-party integrations it carries (each one is a future adaptive-work source), and whether it's in a regulated space where compliance requirements change under you. That range, applied to the build-cost tiers from our [Flutter app development cost breakdown](https://nerdy.pro/blog/flutter-app-development-cost-2026): | Tier | Typical build cost | Typical annual maintenance | | --- | --- | --- | | MVP | $15,000 – $30,000 | $2,250 – $7,500 | | Business app | $30,000 – $60,000 | $4,500 – $15,000 | | E-commerce | $50,000 – $90,000 | $7,500 – $22,500 | | Enterprise / AI | $90,000+ | $13,500+ | Those maintenance figures are the rule of thumb applied to the build-cost ranges, not a separately quoted number — treat them as a planning estimate, not a quote. The first year after launch tends to run toward the higher end of the range: you're still finding corrective bugs real users surface that QA didn't, and the app hasn't stabilized yet. Years two and three, for a well-built app, tend to settle lower. For how those build-cost numbers themselves break down by phase, see our [Flutter development process](https://nerdy.pro/blog/flutter-app-development-process) and [timeline breakdown](https://nerdy.pro/blog/how-long-to-build-a-flutter-app) — maintenance is already the last phase in both. ### What drives the cost Some of these apply to any app, mobile or not. A few are specific to shipping on Flutter. | Driver | Flutter-specific? | Why it costs money | | --- | --- | --- | | Flutter SDK upgrades | Yes | Flutter ships several stable releases a year; staying current is cheap, falling two majors behind is not. | | iOS / Android OS updates & store policy | No | Apple and Google each ship a major OS version annually plus minor updates, and revise store review and API requirements multiple times a year. | | Dependency / package churn | Partly | pub.dev packages get abandoned or ship breaking changes; native apps face the same risk in their own package ecosystems. | | Third-party SDK subscriptions | No | Analytics, push notifications, payments, maps — recurring vendor cost, independent of your framework. | | Backend / hosting / API costs | No | Scales with usage, not with what the client app is written in. | | Bug fixes (corrective) | No | Every app ships with bugs; the question is how expensive each one is to find and fix. | | Security patches | No | Both the app and its dependencies need patching as vulnerabilities surface. | | Analytics / monitoring | No | Crash reporting and usage analytics are how you find problems before support tickets do. | | Feature iteration (perfective) | No | The largest, most variable line item — scales with how actively you're still building. | The Flutter-specific rows are the smaller half of the table. Most of what you pay for after launch has nothing to do with which framework built the app — it's the cost of running software at all. ### Where Flutter saves (and where it doesn't) The honest version of "Flutter is cheaper to maintain" is narrower than the marketing version: **one codebase means one upgrade stream, one QA pass, and one release, instead of two.** A native shop maintaining iOS and Android separately pays for the same bug fix, the same SDK bump, and the same regression pass twice — once per platform, on two different schedules, often by two different engineers who have to stay in sync. It's the same mechanic behind the [30–40% lower build cost](https://nerdy.pro/blog/hire-flutter-developers-2026) Flutter has versus two native apps, and per our own [Flutter vs. native comparison](https://nerdy.pro/blog/flutter-vs-native-2026), the saving usually gets *larger* after launch, not smaller — every later fix and feature is still written once instead of twice. Where it doesn't fully hold: platform-specific integrations — deep ARKit/ARCore work, certain payment SDKs, background-processing edge cases — still require native know-how even inside a Flutter app, because Flutter is calling out to native code at that point, not replacing it. And a Flutter app carries real dependency risk the same way a native app depends on its own libraries: a plugin can go unmaintained or get sunset by its vendor, and replacing it is genuine adaptive work. Flutter reduces how often you pay maintenance cost twice; it doesn't remove maintenance cost. ### Why a badly-built app costs more to maintain Every one of the four maintenance buckets gets more expensive when the underlying code is bad — and "bad" here doesn't mean the app looks broken to a user. It means: no tests, so every fix risks a new regression. No consistent architecture, so a change in one screen has side effects on three others nobody predicted. Duplicated logic, so the same bug gets fixed once and reappears somewhere else six months later. This is not a hypothetical. We've [audited dozens of AI-built codebases](https://nerdy.pro/blog/ai-code-audit-findings) and found the same eleven problems recur almost every time: exposed secrets, no input validation, auth that never checks who is asking, no tests, duplicated code, no consistent architecture, callback hell instead of proper async patterns, and no deployment-environment awareness. None of that is visible in a demo. All of it shows up the first time you try to change something safely. Our [review of Flutter written by unsupervised AI agents](https://nerdy.pro/blog/ai-agents-struggle-with-flutter) turns up the same pattern from a different angle — code that works today and gets measurably harder to touch every month it goes unreviewed. The practical takeaway: a $15K MVP built fast and never reviewed is not a $2.3K-a-year maintenance app. It's a rewrite candidate wearing a maintenance budget. If you inherited a codebase — AI-built, outsourced, or otherwise — and don't know which one you have, that's what an [AI code audit](https://nerdy.pro/services/ai-code-audit) is for: it tells you your actual maintenance load before you commit to a year of it. ### Who does the maintenance Three real options, same tradeoffs as hiring for the initial build — covered in more depth in our [in-house vs. agency vs. staff augmentation breakdown](https://nerdy.pro/blog/hire-flutter-developers-2026): - **In-house.** Makes sense once the app is core to the business and you want to own the code and the institutional knowledge full-time. Highest fixed cost, best long-term ownership. - **Agency retainer.** Someone else carries the on-call burden and the Flutter/platform expertise; you pay for capacity you use. Lowest management overhead, least control over exactly who touches the code week to week. - **Staff augmentation.** Engineers work inside your team, your repo, your process — you manage the roadmap, they supply the Flutter capacity. Works well when you already have engineering management but need more hands, via [team augmentation](https://nerdy.pro/services/team-augmentation). We run our own ongoing support as a monthly-hours retainer scoped to the app, not a fixed package — a fintech app with three payment SDKs and a content app with none don't belong in the same maintenance plan, and we'd rather scope it accurately than sell you a number that doesn't match your app. ### How to reduce maintenance cost None of this is exotic; it's mostly discipline that's cheap to keep and expensive to skip: - **Keep dependencies current every quarter, not every two years.** One Flutter version bump is a day of work. Three years of deferred bumps at once is a project. - **Invest in tests and CI early.** A regression caught in CI costs minutes. The same regression caught by a user costs a support ticket, a hotfix, and trust. - **Budget preventive work explicitly**, as its own line item, not as whatever's left over after features ship. It's the bucket that gets cut first and costs the most to have skipped. - **Be wary of fragile or thinly-maintained third-party SDKs.** Every one you add is a future adaptive-work source outside your control — check maintenance activity before you depend on a package, not after it breaks. - **Monitor before your users report.** Crash reporting and analytics turn a silent failure into a fixable one before it becomes a review. ### Frequently asked questions #### How much does it cost to maintain a Flutter app? Plan on roughly 15–25% of the app's initial build cost per year. For an app that cost $30,000 to build, that's about $4,500–$7,500 a year, covering bug fixes, OS and SDK updates, security patches, hosting, and ongoing improvements. #### What percentage of the build cost is annual maintenance? 15–25% per year is a reasonable planning range. Apps with more third-party integrations, compliance requirements, or active feature development tend to run toward the higher end; simpler, more stable apps tend to run lower. #### Is Flutter cheaper to maintain than native? Generally yes, because one codebase means one upgrade, QA, and release cycle instead of separate iOS and Android streams. It's not a full exemption, though — platform-specific integrations and plugin risk still require native expertise inside a Flutter app. #### What's included in app maintenance? Four categories: corrective (bug fixes), adaptive (keeping up with OS, SDK, and API changes), perfective (improvements and new features), and preventive (refactoring, security patching, and tech-debt paydown before it compounds). #### How often does a Flutter app need updating? Flutter ships several stable releases a year, and iOS and Android each ship a major OS update annually plus multiple minor releases and store policy changes. An app that goes a year untouched accumulates adaptive work that gets more expensive the longer it's deferred. #### Can a cheap app be expensive to maintain? Yes. A rushed or unreviewed build — no tests, no consistent architecture, duplicated logic — makes every future bug fix and update riskier and slower, which shows up as inflated maintenance cost regardless of how little the initial build cost. #### Do you offer ongoing Flutter app maintenance? Yes. We support Flutter apps on a monthly-hours retainer scoped to the specific app, either through staff augmentation or a direct support engagement, rather than a fixed one-size package. ### Get a maintenance plan that matches your app The fastest way to a real number isn't a rule of thumb — it's a look at your actual codebase. If you already have a live Flutter app and want to know what it'll really cost to keep running well, we'll scope a maintenance plan against it. If you're not sure what you inherited, start with an [AI code audit](https://nerdy.pro/services/ai-code-audit) and we'll tell you honestly what shape it's in. And if you're still in the build phase, our [Flutter app development](https://nerdy.pro/services/flutter-app-development) team builds with the next three years in mind, not just the launch date. [Get in touch](https://nerdy.pro/contact) and tell us where your app is today. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that builds and maintains apps across fintech, healthcare, and retail.* ## How End-to-End Encryption Actually Works: Key Exchange, the Double Ratchet, and Its Real Trade-offs https://nerdy.pro/blog/how-end-to-end-encryption-works End-to-end encryption is usually explained in one sentence: only the sender and the recipient can read the message. That sentence is correct and almost useless — it describes the guarantee without explaining how two devices that have never met, communicating over a server neither of them trusts, actually establish a shared secret and keep it safe message after message, year after year. This article opens that up: how the initial handshake works when the recipient is offline, how the encryption key changes on every single message, and what E2EE deliberately leaves unprotected. ### Key takeaways - E2EE is established through a **key agreement protocol**, not a shared password — the Signal Protocol's **X3DH** handshake lets two devices agree on a secret even when one of them is offline at the time. - Once a session exists, the **Double Ratchet** algorithm derives a brand-new key for every message, giving **forward secrecy** (past messages stay safe if a key leaks) and **post-compromise security** (the session heals itself after a compromise). - Group chats cannot just run the same handshake pairwise at scale — **Sender Keys** trade a small amount of forward secrecy for encrypting each message once instead of once per recipient. - E2EE protects **content**, not **metadata** — who talked to whom, when, and how often is still visible to the server in most implementations. - The hardest unsolved problem in production E2EE is not cryptography, it is **key verification**: proving the public key you received actually belongs to your contact, not to an attacker in the middle. ### What "established" actually means Symmetric ciphers like AES need both sides to already hold the same key. That works fine once a shared secret exists, but it does not explain how it got there in the first place — you cannot send the key over the same channel you are trying to protect, and you cannot expect two strangers' apps to have pre-shared anything. Establishing E2EE means solving that bootstrap problem: deriving a shared secret between two devices using only public information, over a network controlled by a party neither device trusts. The mechanism almost every modern secure messenger uses is the **Signal Protocol**, designed by Trevor Perrin and Moxie Marlinspike. It runs Signal itself, WhatsApp, and the encrypted tier of Google Messages (RCS). Its handshake is called **X3DH** — Extended Triple Diffie-Hellman — and it solves a problem that a plain Diffie-Hellman exchange cannot: starting a session with someone who is not online right now. ### The X3DH handshake: agreeing on a secret with someone who is offline A phone call requires both parties to pick up at the same time. A text message does not — you send it, and it waits. Messaging apps need to build encryption with the same asynchronous property: Alice should be able to start an encrypted conversation with Bob even if Bob's phone is off. X3DH achieves this by having each user pre-publish a small bundle of public keys to the server *before* they need them: 1. **Identity key (IK)** — a long-term key pair that identifies the device. It rarely changes and is what safety-number verification ultimately checks. 2. **Signed prekey (SPK)** — a medium-term key pair, rotated periodically (the X3DH spec suggests an interval on the order of weeks to a month), signed by the identity key so a recipient can verify it actually came from that device. 3. **One-time prekeys (OPK)** — a batch of single-use key pairs. The server hands one out per new incoming session and discards it after use, and each device periodically uploads fresh ones to replenish the pool. When Alice wants to message Bob for the first time, she fetches one of these bundles from the server (never a private key — only the public halves). She then computes **three or four separate Diffie-Hellman exchanges** between combinations of her own keys and Bob's: - `DH1` = her identity key with Bob's signed prekey - `DH2` = her ephemeral (freshly generated, one-time) key with Bob's identity key - `DH3` = her ephemeral key with Bob's signed prekey - `DH4` = her ephemeral key with Bob's one-time prekey, if one was available She concatenates the results and runs them through a key derivation function (HKDF) to produce a single shared secret. Bob, once he comes online, has all the private halves needed to compute the identical value himself — DH is commutative in exactly the way that makes this work. Neither party ever transmits the secret itself; they each independently *compute* the same number from a mix of long-term, medium-term, and one-time keys. Why four separate DH computations instead of one? Each one buys a specific property: - `DH1` and `DH2` bind the session to both parties' long-term identities — this is what makes the exchange **authenticated**, not just secret. - `DH3` (and `DH4` when a one-time key was available) adds fresh, disposable material so that if a long-term identity key is later compromised, past sessions negotiated before the compromise cannot be recomputed. This is the beginning of forward secrecy, before the ratchet even starts. - The one-time prekey specifically defends against a server that lies about the signed prekey — using each OPK exactly once limits how much a malicious or compromised server can replay. The output of X3DH is a single 256-bit shared secret. On its own, using this one secret for every message for the life of the conversation would be exactly the "one leaked key exposes everything" problem this article opened with. That secret is not the end of the story — it is the seed for the Double Ratchet. ### The Double Ratchet: a new key for every message A ratchet, mechanically, only turns one way. The Double Ratchet algorithm borrows that name deliberately: the encryption state only ever moves forward, and there is no way to wind it back to recover a previous key from a later one. It combines two ratchets running together: **The symmetric-key ratchet.** Each side maintains a "chain key." Every time a message is sent, the current chain key is run through a hash function (HMAC) to produce two outputs: a **message key**, used to encrypt that one message and then discarded, and a **new chain key**, which replaces the old one. Message keys are never reused and cannot be derived from each other in reverse — a hash function only runs forward. If an attacker records ciphertext and later steals a chain key, they can decrypt every message sent *after* that point, but nothing before it. **The Diffie-Hellman ratchet.** The symmetric ratchet alone still has a weakness: a stolen chain key compromises everything going forward indefinitely. To fix that, every time the conversation "turns" — roughly, every time the other party replies — each side generates a fresh ephemeral DH key pair, performs a new Diffie-Hellman exchange with the other party's latest public key, and mixes the result into the chain key. This means a new piece of fresh, unpredictable entropy enters the system on a regular cadence that an attacker cannot predict or precompute. The combination gives two distinct guarantees that are often conflated but are not the same thing: - **Forward secrecy** — compromising today's key does not expose yesterday's messages. Provided by the symmetric ratchet: old chain keys are already gone, hashed forward and discarded. - **Post-compromise security** (the Double Ratchet spec calls this "break-in recovery"; also informally called "self-healing") — compromising today's key does not expose *tomorrow's* messages either, because the next DH ratchet step injects new randomness the attacker never saw. Provided by the DH ratchet. Together they mean a single point-in-time compromise — a stolen device, a coerced key — has a *blast radius*, not a permanent break. The conversation heals itself within a message or two of fresh DH exchanges, which is a materially different security model from "the key is compromised, the conversation is compromised forever." ### Group messaging: why the pairwise protocol does not scale X3DH plus the Double Ratchet describes a one-to-one session. A group of 200 people does not have "a session" — it has up to 200×199 potential pairwise sessions, and encrypting every message once per recipient would mean 200 separate encryption operations (and 200x the bandwidth) for a single text. Signal's answer is **Sender Keys**. Instead of pairwise ratchets between every member, each participant generates one symmetric "sender key" and distributes it to every other group member individually, over their already-established pairwise Double Ratchet sessions (this initial distribution is the expensive, once-per-membership-change part). After that, the sender encrypts a group message exactly **once** with their sender key, and every member who has received that key can decrypt it directly — no per-recipient encryption on the hot path. This buys huge performance gains at the cost of some ratchet properties: a sender key does not roll forward per-message the same way a pairwise Double Ratchet chain does, so it provides weaker forward secrecy within its own lifetime, and it must be explicitly rotated whenever membership changes — someone leaving a group means every remaining member has to regenerate and redistribute a new sender key, or the departed member's copy would still decrypt future messages. This is precisely why removing someone from a large, high-churn group is measurably more expensive than sending them a normal message — the rotation is doing real cryptographic work, not just a database update. ### The problem cryptography alone cannot solve: verifying keys Everything above assumes Alice actually received *Bob's* public keys from the server, not an attacker's. If the server (or anyone who can act as it) hands Alice an attacker-controlled key bundle instead, X3DH runs perfectly and produces a perfectly valid shared secret — with the wrong person. This is a classic **man-in-the-middle** attack, and no amount of key derivation math fixes it, because the math never checks *whose* key it received. The mitigation is out-of-band verification: Signal and WhatsApp render a **safety number** (a fingerprint derived from both parties' identity keys) that users can compare in person, by voice, or by scanning a QR code. If the numbers match, the identity keys are genuine and no one is intercepting the exchange. This step is optional, most users never do it, and that gap — not the cryptography — is where the realistic attacks against E2EE messengers actually happen: a compromised or coerced key-distribution server, not a broken cipher. ### What this buys you, and what it does not **Pros:** - **Confidentiality survives server compromise.** Because the server only ever holds ciphertext (and, in X3DH, public keys), a breached database, a subpoenaed backup, or a rogue employee cannot expose message content — there is nothing readable to hand over. - **Forward secrecy limits the damage of key theft.** A stolen device or a compromised long-term key does not retroactively unlock a user's entire message history. - **Post-compromise security means the system recovers.** Unlike a single static key, a compromised Double Ratchet session repairs itself within a few messages. - **Group messaging scales without per-recipient re-encryption**, thanks to Sender Keys — usable performance at sizes where pairwise encryption would fall over. **Cons:** - **Metadata is untouched.** The server still sees who is messaging whom, how often, from what IP, and for how long — [research consistently shows](https://www.eff.org/deeplinks/2013/06/why-metadata-matters) metadata alone reveals sensitive relationship and behavior patterns, sometimes more reliably than content would. - **Endpoint compromise bypasses everything.** E2EE protects data in transit and at rest on the server — not on a device already compromised by malware or physical access. Decrypted messages are, by definition, readable on the device that decrypted them. - **Key verification is manual and mostly skipped.** The protocol is only as strong as its weakest link, and for most users that link is "I never checked the safety number." - **It is expensive to bolt on after the fact.** Features that assume server-side visibility — search, spam detection, link previews, content moderation, backups — all have to be rebuilt or redesigned around ciphertext the server cannot read. This is exactly the migration Meta's engineering team [documented for Messenger](https://engineering.fb.com/2024/02/22/security/end-to-end-encryption-messenger-update/), and it is also why Instagram's 2026 rollback of E2EE from Direct Messages was a product and regulatory decision, not a cryptographic one — the mechanism this article describes was already built and working. We cover that reversal, and how E2EE fits alongside AES, TLS, and post-quantum cryptography in a wider security architecture, in [Encryption Explained](https://nerdy.pro/blog/encryption-explained). - **Group membership changes cost real cryptographic work.** Removing a member requires rotating and redistributing sender keys, not flipping an access-control flag. ### Designing for it, not bolting it on The recurring lesson across every production E2EE system — Signal, WhatsApp, iMessage — is that the parts of a messaging product that seem unrelated to cryptography (search, notification previews, spam filters, backups, multi-device sync) are the parts that actually determine whether E2EE is feasible. A key exchange protocol and a ratchet are well-understood, publicly reviewed building blocks at this point; the hard engineering is everywhere the server used to have visibility and now does not. If you are building or auditing a product that handles sensitive user data — health records, financial communications, legal correspondence — the design decision about end-to-end encryption has to happen at the architecture stage, not as a retrofit. That is exactly the kind of gap we look for in an [AI-generated code audit](https://nerdy.pro/services/ai-code-audit): places where a feature quietly assumes server-side access to plaintext that the security model promised would never exist. If you want a second set of eyes on how encryption fits into your application's architecture, [get in touch](https://nerdy.pro/contact). ### References - Perrin, T. & Marlinspike, M. (2016). [The X3DH Key Agreement Protocol](https://signal.org/docs/specifications/x3dh/). Signal. - Perrin, T. & Marlinspike, M. (2016). [The Double Ratchet Algorithm](https://signal.org/docs/specifications/doubleratchet/). Signal. - Cohn-Gordon, K. et al. (2016). [A formal security analysis of the Signal messaging protocol](https://eprint.iacr.org/2016/1013.pdf). Cryptology ePrint Archive. - WhatsApp (2024). [WhatsApp Security Whitepaper](https://www.whatsapp.com/security/WhatsApp-Security-Whitepaper.pdf). - EFF (2013). [Why metadata matters](https://www.eff.org/deeplinks/2013/06/why-metadata-matters). - Meta Engineering (2024). [End-to-end encryption on Messenger](https://engineering.fb.com/2024/02/22/security/end-to-end-encryption-messenger-update/). ## How Long Does It Take to Build a Flutter App? (2026 Timeline Breakdown) https://nerdy.pro/blog/how-long-to-build-a-flutter-app **TL;DR.** A Flutter MVP typically takes **6–12 weeks** from kickoff to a live store listing. A standard business app takes **3–6 months**. A complex or regulated app — fintech, healthcare, anything an auditor will read — takes **6–12+ months**. The variable that moves those numbers most is not how fast anyone writes code: it is **scope, and how quickly decisions get made on the client side**. The phases also overlap, so the calendar is always shorter than the sum of the phases — which is why "how long" and "how much work" are two different questions. Every agency answers this with "it depends." It does — but that is not something you can plan a launch around. Here are the numbers we quote, what sits inside them, and what makes them slip. ### How long does a Flutter app take, by complexity? Three tiers cover almost everything we get asked to build. Durations are kickoff to store listing, assuming a signed scope and reasonable access to whoever makes decisions. | Tier | Typical duration | Example feature set | Team | Biggest timeline driver | | --- | --- | --- | --- | --- | | **MVP** — validate the idea | **6–12 weeks** | 5–8 core features, iOS + Android, Firebase or a light REST backend, component-library UI, basic analytics | 1–2 Flutter engineers, part-time designer, lead engineer | Scope discipline. Every "small addition" is a week. | | **Standard business app** | **3–6 months** | 15–20 features, custom design system, custom backend, admin dashboard, segmented push, offline, payments | 2–3 Flutter engineers, backend engineer, designer, QA | Integrations and backend readiness — usually not the app | | **Complex or regulated** | **6–12+ months** | Compliance work (HIPAA, PCI-DSS, SOC 2), role-based access, real-time data, ERP/CRM integrations, on-device ML | Full team plus DevOps and a PM | Dependencies you do not control: auditors, banks, vendors | Three to five months is the typical business-app build; the sixth month shows up when integrations or compliance arrive late, which they often do. These tiers match the price tiers in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026) — roughly $15–30K for the MVP tier, $30–60K for the business tier. Duration and budget move together: you are buying a team for a number of weeks. ### Where do the weeks actually go? The third column is the important one: these phases do **not** run one after another. | Phase | Typical duration | Overlaps with | | --- | --- | --- | | Discovery & scoping | 3 days – 2 weeks | Gates everything; nothing real starts before it | | UX/UI design | 2–4 weeks | Starts before discovery closes; its tail runs into development | | Architecture & foundations | 1–2 weeks | Design | | Core development | 4–16 weeks | Design tail, QA, backend work | | Integrations | 1–2 weeks per major integration | Core development | | QA & testing | Continuous, plus a 1–2 week hardening pass | Core development | | Store submission & review | 1–2 weeks (review itself is usually 24–48h) | Runs last; assets prepared earlier | Add those rows up naively and you get eight months for a build that ships in four. The overlap is the difference: design finishes screens while engineers build the ones already signed off, QA runs inside each sprint, and the backend is load-tested against a mock client. For what happens inside each phase, see the companion post on [the Flutter app development process](https://nerdy.pro/blog/flutter-app-development-process) — this post is the *how long*, that one is the *how*. ### How long to build an MVP vs a full product? An MVP takes 6–12 weeks because an MVP is a question, not a product: 5–8 features, component-library UI, a backend you did not write from scratch, and a cut list you actually honour. That shape is what our [MVP development](https://nerdy.pro/services/flutter-app-development/mvp) service is scoped around. The 6-week end is real but conditional. It needs a scope that fits on one page, one person who can approve a decision without a committee, and no novel technical risk — no video pipeline, no on-device ML, no payment rails in a new jurisdiction. Migrating a Telegram bot with an existing user base into a native app was [six weeks](https://nerdy.pro/blog/telegram-bot-to-app-6-weeks), because the bot had already validated the product and the scope was fixed. Ten to twelve weeks is more common, and the extra month is almost never "the code was harder" — it is a payment flow that appeared in week four, an unbudgeted design round, or two weeks waiting on API access. A full product takes 3–6 months for a reason unrelated to feature count: it is built to survive real usage — custom design system, real business logic, offline behaviour, named error states, tests in CI, an admin panel for whoever runs support. That is the work separating an app you can grow from one you rewrite in eighteen months. ### What actually determines the timeline? Six things, in rough order of how much damage they do. **1. Scope.** By a distance. "A simple app" becomes 40 screens once onboarding, auth, settings, account deletion, payments, push and offline are counted — all of which were always in the app you described. Two extra features do not add two weeks of coding; they add coding, design, QA and a fresh set of edge cases. **2. Backend readiness.** If the API exists, is documented and is stable, the Flutter team runs at full speed. If another team is writing it in parallel, your timeline is now their timeline. **3. Integrations.** Each meaningful one — payments, maps, chat, video, biometrics, a CRM — is one to two weeks. The SDK takes an afternoon; the edge cases take the fortnight. **4. Design maturity.** Arriving with a design system saves two to three weeks, because engineers assemble screens from components that exist. No design at all is not faster — it moves the work into development, where it costs more. **5. Store review.** One to two weeks for the submission phase, most of it your work rather than Apple's. More below. **6. Decision latency — usually you.** A build has dozens of open questions, each waiting on someone. Ten of them answered in four days instead of one adds six weeks to a project where nobody wrote a slow line of code. On a healthy project the client is the most common bottleneck — and the cheapest one to fix. ### Does Flutter build faster than native? Yes — but be precise about what gets saved, because this is where agencies overclaim. Flutter cuts **build effort** by roughly **30–40%** versus shipping two native apps, and the gap widens over the product's life because every later feature is written once instead of twice. We broke down where that number comes from in [Flutter vs Native in 2026](https://nerdy.pro/blog/flutter-vs-native-2026). What it does **not** do is halve the calendar. Discovery does not shorten because you picked Flutter, design happens once either way, QA still needs real devices on both platforms, and the backend does not care what the client is written in. A six-month two-native build becomes roughly a four-month Flutter build — not a three-month one — with one team instead of two and one codebase to maintain afterwards. Flutter is how you get two platforms for close to the price of one. It is not how you get a six-month app in six weeks. ### How much time does App Store and Play review add? Budget **one to two weeks** for the whole submission phase and you will rarely be wrong. The review itself is the small part. Apple review is **typically 24–48 hours** once you submit. Typical is not guaranteed, and the rejection reasons are predictable enough to design around: missing account deletion, a sign-in wall with no demo credentials, an incomplete privacy declaration, payments routed around in-app purchase. Google review is usually faster, though a brand-new developer account can sit in extended review for days. The time that actually goes missing is the rejection cycle — a 24-to-72-hour round trip, and a first submission that gets one is normal. Plan for one and you absorb it; plan for none and it lands on your launch date. Two cases run longer: regulated data pulls extra questions, and shipping multiple branded apps from one codebase makes Apple guideline **4.2.6** its own discipline — same screens with a new logo get rejected as duplicates. We wrote up [what actually passes review at fleet scale](https://nerdy.pro/blog/white-label-app-platform-flutter); designing for it upfront is the difference between a one-week submission and a month of appeals. ### Where do timelines slip? Four failure modes account for nearly every overrun we have seen. **Scope creep arriving as small requests.** No single "can we also…" is unreasonable. Six of them are a month. **APIs that land late.** The most common cause of a Flutter team sitting idle. Mocks buy a few weeks, then stop buying anything. **Design by review cycle.** Three rounds of "let us see one more option" on the home screen delays every phase behind it, because engineers cannot build against a moving target. **Indecision, including silence.** A sprint nobody on the client side installs is a sprint whose misunderstandings surface a month later. Three of those four sit on the client side of the table. That is not blame-shifting — it is where the leverage is. An agency can compress engineering by maybe 10%; a decisive client can compress the calendar by 30%. ### The ship-faster checklist Six levers, all of them yours to pull: 1. **Fix the scope in writing, and keep a cut list.** The features explicitly *not* in version one are the most valuable artifact in the project. 2. **Name one decision-maker** who can approve a design or resolve a question the same day. 3. **Bring the API, or accept that it is on the critical path.** If the backend is being built in parallel, contract the interface early and freeze it. 4. **Arrive with a design system** — palette, type ramp, component states — or budget three weeks to build one. Skipping it just moves the cost. 5. **Set up store accounts, privacy declarations and demo credentials in week one**, not the week you submit. 6. **Add capacity instead of pressure**, and add it early. [Hiring Flutter developers in 2026](https://nerdy.pro/blog/hire-flutter-developers-2026) covers the models; [team augmentation](https://nerdy.pro/services/team-augmentation) is how we add engineers to an existing team in days rather than months. One anti-lever: developers added in the final month make a project later, not earlier — they learn the codebase from whoever was already the bottleneck. ### Frequently asked questions #### How long does it take to build a Flutter app? A Flutter MVP typically takes 6–12 weeks from kickoff to a live store listing. A standard business app with a custom design system, a real backend and an admin panel takes 3–6 months. A complex or regulated app takes 6–12 months or more. Scope and client decision speed move those numbers more than engineering speed does. #### How long does it take to build an MVP? Six to twelve weeks is the realistic range for a Flutter MVP in 2026. The 6-week end requires a scope that fits on one page, a single decision-maker, and no novel technical risk such as video, on-device ML or new payment rails. Ten to twelve weeks is more common, and the extra time usually comes from scope added mid-build or waiting on API access rather than from slow coding. #### Is Flutter faster to build than native? Yes, for the large majority of apps. One codebase for iOS and Android cuts build effort by roughly 30–40% compared with two native apps, and the saving grows over time because every later feature is written once instead of twice. The calendar shrinks less than the effort does: discovery, design, QA, backend work and store review do not halve just because the client is cross-platform. #### How long does App Store review take? Apple review is typically 24–48 hours after submission, and Google Play is usually faster, though a brand-new developer account can sit in extended review for days. Budget one to two weeks for the whole submission phase: most of that time is store assets, privacy declarations and the possibility of one rejection cycle. First submissions commonly get rejected for a predictable reason — missing account deletion, no demo credentials, or an incomplete privacy declaration. #### Can you build a Flutter app in a month? You can build something real in a month, but not a full product. Four weeks is enough for a sharply scoped prototype or a single-flow app on a hosted backend with library UI — useful for a demo, a pilot or an investor conversation. A store-ready MVP with authentication, payments and the settings screens Apple requires needs 6–12 weeks. #### What slows down app development the most? Scope growth and slow decisions, in that order. Features added mid-build cost design, development and QA time rather than just coding time, and questions that take four days to answer instead of one add up across the dozens a build requires. Late backend delivery is third: when the API arrives in week eight of a twelve-week project, no amount of engineering speed recovers the calendar. #### Does a bigger team build the app faster? Up to a point, and only if the team is sized correctly from the start. An MVP runs well with one or two Flutter engineers; a third rarely makes it faster, because the work does not split cleanly at that size. Adding people late makes a project slower, because onboarding pulls time from the engineers who were already the constraint. ### Want a timeline for your specific app? Generic ranges are fine for budgeting a quarter and useless for planning a launch. What you need is a number for *your* feature list, backend situation and store constraints. Send us what you are building and we will come back with a scoped timeline: the phases, what runs in parallel, what we would cut to hit a date, and which parts depend on you rather than on us. If your date is not achievable, we will say so before you sign anything. That is our [Flutter app development](https://nerdy.pro/services/flutter-app-development) practice: fixed scope, demoable builds every one to two weeks, and a timeline we will defend — see [ExtraETF](https://nerdy.pro/portfolio/extraetf), real-time market data in a regulated market, and [Arcana](https://nerdy.pro/portfolio/arcana), an AI chat product shipped to the store. [Tell us what you are building](https://nerdy.pro/contact) and we will give you the date, not a range wide enough to hide in. --- *Dima is Lead Flutter Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that takes apps from discovery to the App Store across fintech, retail, and AI products.* ## The Flutter App Development Process: How We Take an App From Idea to App Store https://nerdy.pro/blog/flutter-app-development-process **TL;DR.** The Flutter app development process runs in seven phases: **discovery and scoping**, **UX/UI design**, **architecture and foundations**, **iterative development**, **QA and testing**, **release**, and **post-launch maintenance**. What ties them together is that they are *feedback loops, not a waterfall* — design starts before discovery finishes, QA runs alongside the build rather than after it, and every two weeks you get something installable on your phone instead of a status report. The one rule we do not bend: **the invariants come before the feature code.** The state model, theme, error taxonomy, CI pipeline and test strategy are decided in week one, because every screen built afterwards either follows them or fights them. A typical build is 2–3 months for an MVP and 3–5 months for a full product, and launch is the middle of the process, not the end. --- ### The Flutter app development steps at a glance Seven phases, in the order they start. This is the whole Flutter development lifecycle on one screen. | Phase | What happens | Key deliverable | Who's involved | Typical duration | | --- | --- | --- | --- | --- | | **1. Discovery & scoping** | Idea becomes a specific, ordered backlog; the cut list gets agreed | Scoped backlog, fixed-scope quote, timeline | Founder/product lead, lead engineer, designer | 3 days – 2 weeks | | **2. UX/UI design** | Design system first, then flows and screens against it | Design system + clickable prototype | Designer, product lead, lead engineer | 2–4 weeks (overlaps build) | | **3. Architecture & foundations** | State model, folder structure, error taxonomy, CI/CD, test strategy | Running app skeleton, green pipeline, ADRs | Lead engineer, backend engineer | 1–2 weeks | | **4. Iterative development** | Vertical feature slices, demoable every sprint | Installable build every 1–2 weeks | Engineers, designer, product lead | 4–16 weeks | | **5. QA & testing** | Widget, golden, integration, and real-device testing | Test suite in CI + hardening pass | Engineers, QA, client testers | Continuous + 1–2 week hardening | | **6. Release** | Store assets, submission, review, staged rollout | Live app on App Store and Google Play | Lead engineer, product lead, client accounts | 1–2 weeks | | **7. Post-launch** | Monitoring, crash triage, iteration on real usage | Monthly release cadence, crash-free rate | Engineers, product lead | Ongoing | Those durations are ranges, and they overlap — the calendar is shorter than the sum of the rows. For where the weeks actually go on a real project, see the companion post: [how long it takes to build a Flutter app](https://nerdy.pro/blog/how-long-to-build-a-flutter-app). For what each phase costs, see [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026). Structurally, this is the same mobile app development process any competent team runs — the phases are not a Flutter invention. What Flutter changes is the economics inside them: one codebase to design for, one to test, one to release, and a UI layer that lives in code rather than in a stylesheet. That last detail is why the design and architecture phases carry more weight here than they would elsewhere. --- ### 1. Discovery & scoping Discovery is the phase where an idea becomes a specific, ordered list of things to build. It is not a workshop with sticky notes. It is a lead engineer and a designer interrogating the app until every screen has a reason to exist. Three things come out of it: - **A backlog, ordered.** Not "user management" — "sign in with Apple, sign in with email, password reset, account deletion." Vague line items are where estimates go to die. - **A cut list.** The features that are *not* in version one, written down and agreed. The most valuable artifact in the process, and the one clients resist hardest. - **A fixed-scope quote and timeline**, derived from the backlog rather than from a gut feel about "an app like this." This is where budgets and timelines are really set, which is why a rushed discovery is the most expensive way to save a week. When you describe "a simple workout tracker" and we hand back a 40-screen backlog, that gap is not padding — onboarding, auth, settings, account deletion, payments, push and offline were always in the app you described. The [cost breakdown](https://nerdy.pro/blog/flutter-app-development-cost-2026) covers how scope compounds into a number. **Where it breaks:** discovery with someone who cannot decide. If every answer goes back to a committee, discovery stretches from one week to four and the estimate is built on sand. --- ### 2. UX/UI design UX/UI design is the phase where the app's visual and interaction language gets defined. It starts with the system, not the screens. The first deliverable is a **design system**: a palette with light and dark variants, a type ramp, a spacing scale, component states (default, pressed, disabled, loading, error), and the interaction vocabulary — how *this app* shows loading, shows an empty list, confirms a destructive action. Screens get designed against that system afterwards. That order is not aesthetic preference; it is build speed. A Flutter app renders every pixel from Dart code, and a mature design system maps almost one-to-one onto `ThemeData` — wired in once, every later screen is assembled from components that already exist. Without it you get what we describe in [how AI agents struggle with Flutter](https://nerdy.pro/blog/ai-agents-struggle-with-flutter): forty screens that each work and do not add up to one app, colors hardcoded at every call site, and a rebrand that costs a several-hundred-file diff. We design for the constraints that break Flutter layouts later, too — long strings in the second locale, 200% text scale, small phones at 320pt. Catching those in Figma costs minutes; catching them in QA costs a redesign. What you get out of the phase is a design system plus a clickable prototype, before a line of feature code exists. **Where it breaks:** design-by-review-cycle. Three rounds of "can we see one more option" on the home screen delays every downstream phase, because engineers cannot build against a moving target. --- ### 3. Architecture & foundations Architecture is the phase where the decisions every later screen depends on get made — before any screen exists. Practically, that means we spend one to two weeks building an app that does nothing. That fortnight decides the next six months. What gets settled: - **State management.** One choice, applied everywhere. Sealed state classes, so illegal combinations — loading *and* error at once — are unrepresentable rather than merely unlikely. - **Folder structure and module boundaries.** Where a feature lives, what it may import. - **An error taxonomy.** Named failure modes — network, auth expired, validation, declined payment, conflict — instead of one `Something went wrong` that makes every bug unreproducible. - **CI/CD.** Pipeline green on day one: `flutter analyze` with lints promoted to errors, `flutter test`, a build artifact per commit, Fastlane wired to TestFlight and the Play internal track. - **Testing strategy.** Which layers get widget tests, which screens get goldens, what the frame-time budget is. We call these the invariants, and they come first because they cannot be retrofitted cheaply. "Follow the existing pattern" is a cheap instruction to give a person or an agent — but only once a pattern exists to point at. In a repo with none, everyone invents their own, and the result is the archaeology we get paid to [audit](https://nerdy.pro/services/ai-code-audit) two years later. The phase ends with a running skeleton, a green pipeline, and short decision records for the choices a future engineer would otherwise re-litigate. --- ### 4. Iterative development Iterative development is the phase where features get built and shipped in small, demoable increments rather than delivered in one lump at the end. The Flutter development workflow here runs in **vertical feature slices**: one feature built end to end — UI, state, API integration, error handling, tests — rather than "all the screens" followed by "all the plumbing." A slice you can open on a phone tells you something true. A folder of unwired screens does not. The cadence is a demoable increment every one to two weeks, on your device via TestFlight or the Play internal track. Not a screenshot, not a Loom — the app, on your phone, that you can hand to a colleague. Code review is on **decisions, not lines**. Is this the same state model as the rest of the app? Is it styled from the theme? Is this value derived once, or for the third time? Does this string survive Russian? Agents write a large share of the code and our engineers own those decisions, which is [how we build](https://nerdy.pro/blog/building-with-ai) — line-by-line review of generated code does not scale; decision-level review does. Design and QA keep running alongside, and so does backend work — which is why a system like the [real-time market data layer behind ExtraETF](https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go) gets built and load-tested while the Flutter client is still assembling screens against a mock. **Where it breaks:** silent sprints. If two weeks pass without you opening a build, the loop is broken and you find out about the misunderstanding a month late. --- ### 5. QA & testing QA is the layer that runs from the first feature slice to the last, not a phase bolted on at the end — automated checks in CI on every commit, plus a hardening pass before release. What runs in CI on every commit: - **Widget tests** for behavior: the button disables while submitting, the error clears on retype, the list paginates at the right offset. - **Golden tests** for appearance — the highest-leverage test type in Flutter, because a golden turns "did this redesign break the empty state at 320pt in dark mode at 200% text scale?" into a check that runs in seconds instead of a question nobody asks. - **Integration tests** for the flows that lose money when broken: sign-up, checkout, payment. - **A regression test for every bug fixed**, so it cannot come back. What a machine cannot do, a person does on real hardware: the app at 200% text scale, in dark mode, in both languages, on a small old Android phone, with the network throttled and then killed mid-request. Ten minutes of that finds an entire category of defect no static check will ever report. And we hold a **frame budget** — 16.6ms at 60fps, checked on profile builds. A list that janks after someone adds a shadow and an `Opacity` inside the item builder is a regression no unit test catches and every user feels. --- ### 6. Release Release is the phase where the app goes through store submission, review, and rollout. It is 1–2 weeks of work that most timelines pretend is an afternoon. **Store assets and metadata** — screenshots at every required size, descriptions, keywords, privacy nutrition labels, data-safety declarations. Account deletion has to exist in-app; both stores enforce it. **Review realities.** Apple review is typically 24–48 hours, but typical is not guaranteed, and the rejection categories are predictable: missing account deletion, a sign-in wall with no demo credentials, an incomplete privacy declaration, payments routed around in-app purchase. For anyone shipping multiple branded apps, guideline 4.2.6 is its own discipline — we covered [what actually passes review](https://nerdy.pro/blog/white-label-app-platform-flutter) at fleet scale. Google review is usually faster, but a new developer account can sit in extended review for days. **Staged rollout.** We ship to 10% on Google Play and watch the crash-free rate before going wider; phased release on the App Store does the same. A bad build caught at 10% is a bad afternoon. The same build at 100% is a bad week. **Automation.** Fastlane builds, signs, and uploads to both stores — including multi-publisher setups where each brand ships under its own account. Manual release is where a version code gets typed wrong at 11pm. --- ### 7. Post-launch & maintenance Post-launch maintenance is the phase where the app meets real users and keeps changing. Launch is the start of it, not the end of the project. Real usage immediately surfaces what no test could: the OEM Android skin that breaks push notifications, the OS update that deprecates an API, the crash on a device you did not own. So the process continues: - **Monitoring** — crash reporting, performance traces, and analytics on the flows that matter, watched hourly for the first 48 hours and weekly after that. - **Iteration** — the backlog you cut in discovery, reprioritized against what people actually do. - **Maintenance** — annual OS releases, SDK updates, store policy changes, certificate and API-key rotation. Flutter and its plugins move; an app left untouched for a year is not stable, it is stale. Budget for the product, not the project. An app that ships and then gets nothing is on a slow path back to a rewrite. --- ### How we keep it on track Processes do not fail dramatically. They slip a few days at a time. Four things prevent most of it, and each has a known failure mode. **Tight scope, written down.** The cut list from discovery is a contract with the timeline; new ideas go on the v2 list and we say so out loud. *Breaks when* nobody defends the list — every "tiny addition" is genuinely tiny, and the twentieth one is a month. **A decisive stakeholder cadence.** One person who can approve a screen the day they see it, a weekly call, a build to install. *Breaks when* decisions route through a committee. On a six-week build, one week of decision latency is a 20% overrun before an engineer has done anything wrong. **Invariants set up front.** The reason our third month costs about what our first did. *Breaks when* a deadline tempts everyone to skip week one and start on screens. **Short feedback loops.** Installable builds every one to two weeks, QA in CI, a demo you actually open. *Breaks when* the client goes quiet — the loop only works if someone is on the other end of it. And the honest one: **our estimates are wrong in one direction.** Integrations are where they miss most. Every third-party SDK looks like an afternoon in the pitch and turns into a two-week debugging session about edge cases on Android 10. We build slack in for that rather than pretending it away, and when a phase is going to overrun you hear it that week, not at the deadline. --- ### Frequently asked questions #### What are the steps to build a Flutter app? There are seven steps: discovery and scoping, UX/UI design, architecture and foundations, iterative development, QA and testing, release, and post-launch maintenance. Discovery turns the idea into an ordered backlog and a fixed scope, design produces a design system and a prototype, and architecture sets the state model, error handling, CI pipeline and testing strategy before any feature code is written. Development then builds features in vertical slices with QA running alongside rather than after, followed by store release and ongoing maintenance. #### How does the Flutter development process work? It works as a set of overlapping feedback loops rather than a linear waterfall. Design starts before discovery finishes, engineering foundations are laid while the last screens are being designed, and QA runs continuously in continuous integration instead of as a phase at the end. The client sees an installable build every one to two weeks, which is the loop that catches misunderstandings while they are still cheap to fix. A typical build is two to three months for an MVP and three to five months for a full product. #### What comes first in app development? Discovery comes first: turning the idea into a specific, ordered backlog with an explicit list of what is not in version one. Budgets and timelines are really set here, not later, because everything downstream is execution against these decisions. On the engineering side, the first thing built is not a feature but the foundations — the state model, folder structure, error taxonomy, CI pipeline and testing strategy — because those cannot be retrofitted cheaply once forty screens exist. #### How is QA done for Flutter apps? QA runs continuously, not as a final phase. Widget tests cover behavior, golden tests catch visual regressions by comparing rendered screens against reference images, and integration tests cover the flows that lose money when broken. On top of that, engineers test on real devices at 200 percent text scale, in dark mode, in every supported language, and on a throttled or dropped network. Frame times are checked against a 16.6ms budget on profile builds, because jank is a regression that no unit test catches and every user feels. #### How long does each phase of a Flutter project take? Discovery takes three days to two weeks, design two to four weeks, and architecture and foundations one to two weeks. Iterative development runs four to sixteen weeks depending on scope, with QA continuous throughout plus a one to two week hardening pass. Release takes one to two weeks including store assets, review and staged rollout. The phases overlap heavily, so the calendar is shorter than the sum of those ranges — typically two to three months for an MVP and three to five for a full product. #### What happens after a Flutter app launches? Launch is the start of the product, not the end of the project. The first 48 hours are spent watching crash reports, performance traces and the staged rollout percentage before widening it. After that the work becomes iteration on real usage data plus maintenance: annual iOS and Android releases, Flutter and plugin updates, store policy changes, and certificate and API key rotation. An app that ships and then receives nothing is not stable, it is quietly heading toward a rewrite. #### How is agile used in Flutter development? It is used as short feedback loops rather than as a ceremony framework. Work is built in vertical feature slices that go from the screen through to the backend, so every increment is something you can install and use rather than a folder of unwired screens. Sprints are one to two weeks and each ends with a build on the client device via TestFlight or the Play internal track. What stays deliberately non-agile is the architecture: the state model, theme, error taxonomy and test strategy are set up front, because iterating on those after forty screens exist is a rewrite, not a sprint. --- ### Scope your build with us That is how Flutter apps are built when someone is actually running the process. If you are evaluating agencies, the process *is* the product — anyone can show you screenshots. What decides whether your app ships on time is whether there is a real machine behind it: a cut list someone defends, invariants set in week one, a build on your phone every fortnight, and tests that run before a human looks. - **Book a discovery call** and we will run your idea through step one: [talk to us](https://nerdy.pro/contact). You get an ordered backlog, a cut list, and a fixed-scope quote — not a range wide enough to hide in. - **See the offering** — team, stack, and pricing tiers on the [Flutter app development](https://nerdy.pro/services/flutter-app-development) page. - **Already have a build?** If someone else ran this process badly, an [AI code audit](https://nerdy.pro/services/ai-code-audit) tells you what it costs to fix in place — almost always less than the rewrite you are being quoted. Tell us what you are building and where you are stuck, and we will tell you which phases will be easy and which will hurt. --- *Dima is Lead Flutter Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that takes apps from discovery to the App Store across fintech, retail, and AI products.* ## How AI Agents Struggle with Flutter: 7 Gaps Experience Still Closes https://nerdy.pro/blog/ai-agents-struggle-with-flutter **TL;DR.** This is not an argument against AI writing Flutter. Agents write a large share of our code, and we [designed our process around that on purpose](https://nerdy.pro/blog/building-with-ai). It is an argument about what happens when nobody is orchestrating them. We ship Flutter with agents, and we audit Flutter that agents wrote unsupervised — and the same seven gaps separate the two. An unsupervised agent **recomputes derived state** everywhere instead of keeping one source of truth; **writes no tests and no benchmarks**, so review is the only gate and regressions ship freely; **collapses complex async into if- and switch-pyramids over loose flags** instead of modelling it as a stream; **treats design tokens as constants in a `constants.dart`** instead of the `ThemeData` the framework already gives it; **invents a fresh UX vocabulary on every screen**, because it cannot see the app; **concatenates strings** where ICU plural rules are mandatory; and **simplifies uniformly**, flattening the parts of the domain where complexity is the point. None of this is a Flutter problem, and none of it means the code is worthless. It means the whole-system decisions are still ours. --- ### Why Flutter punishes this harder than most stacks Every one of these failures shows up in every language. Flutter just makes them louder, for four structural reasons. **The UI is code, all the way down.** There is no stylesheet sitting outside the source to keep things coherent, no cascade, no shared CSS file that a second developer is forced to notice. Every pixel is a Dart expression. If consistency is not enforced *inside* the code, nothing else enforces it. **Everything compiles.** Flutter offers roughly ten reasonable ways to do anything, and all ten run. There is no compiler error for "you should have used the theme here", no warning for "this is the third time you derived this total", no lint for "this screen loads differently from the other thirty-nine". The feedback the agent needs does not exist unless someone builds it. **The agent never sees the frame.** It writes a screen, gets a clean `flutter analyze`, and reports success — on a layout that overflows at 320pt, hides the submit button behind the keyboard, and fails contrast in dark mode. A human notices all three in two seconds of looking. An agent has no eyes unless you give it some. **The training data is a decade of mixed Flutter.** The framework moves fast and the corpus does not. So agents confidently write `RaisedButton` (removed in 3.0), `WillPopScope` (superseded by `PopScope` after 3.12), `MediaQuery.of(context).textScaleFactor` (replaced by `TextScaler` in 3.16), `MaterialStateProperty` (renamed `WidgetStateProperty` in 3.19), `Color.withOpacity` (deprecated for `withValues` in 3.27), and `ThemeData.accentColor`, which was removed in 3.7 and has not existed for years. It is not that the model is wrong — it is that it is answering for a Flutter version nobody ships anymore, and half of it still compiles with a deprecation warning nobody reads. Now the seven. --- ### 1. One source of truth, or recompute it everywhere **What the agent does.** It derives the same value in every place that needs it. The cart total is folded in the cart page. It is folded again in the sticky checkout footer. It is folded a third time in the order summary — and that one applies the discount before tax instead of after. Nothing is *wrong* in any single file; the three just disagree, and the disagreement is invisible until a customer is charged the wrong amount. The Flutter-specific version is worse: mirroring state into a widget. ```dart class _CartPageState extends State { double _total = 0; // copy #2 of the truth @override void initState() { super.initState(); _total = widget.items.fold(0, (sum, i) => sum + i.price * i.qty); } // ...and now _total never updates when the cart does } ``` This is the classic `initState` copy. It works in the demo because the demo never changes the cart after the page opens. It breaks the first time a quantity is edited from a bottom sheet. **What we do.** The value is derived exactly once, in the state layer, and every widget reads it. If it can be computed from state, it is not state. ```dart // One derivation. The cart page, the footer and the summary all read this. Stream get total => _cart.map( (c) => c.items.fold(Money.zero, (sum, i) => sum + i.subtotal), ); ``` Note the `Money` type rather than `double`. Agents reach for `double` on money constantly, and [`0.1 + 0.2 != 0.3` is not academic when it is somebody's balance](https://nerdy.pro/blog/floating-point-and-money). **Why the agent lands there.** Finding the existing derivation costs context; regenerating it inline costs nothing. Each prompt starts fresh, so "is this already computed somewhere?" is a question it structurally cannot ask. Every copy is locally reasonable. The drift is what you pay for. --- ### 2. Tests and benchmarks, or hope **What the agent does.** It ships the feature and stops. No widget tests, no goldens, no benchmark. The definition of done is "it compiles and the screen looks plausible in the description I wrote of it". That leaves code review as the only quality gate — and this is the part people underestimate. Review was already the bottleneck when humans wrote the code at human speed. Point three agents at a codebase and the volume of diff arriving at that gate goes up by an order of magnitude while the number of people qualified to review it stays at one. Review does not scale. Executable checks do. **What we do.** We write the tests and the benchmarks *first*, and not as a virtue exercise — as the feedback loop the agent is missing. - **Widget tests** for behaviour: the button disables while submitting, the error clears on retype, the list paginates at the right scroll offset. - **Golden tests** for appearance. This is the single highest-leverage thing you can hand an agent working on UI, because a golden is the closest thing to giving it eyes. `matchesGoldenFile` turns "did the redesign break the empty state at 320pt in dark mode with 200% text scale?" into a check that runs in CI in four seconds. Without goldens, that question is only ever answered by a human opening the app — which is to say, sometimes. - **Benchmarks** for the things that quietly rot. The frame budget is 16.6ms; a list that janks after someone adds a shadow and an `Opacity` inside the item builder is a regression no unit test catches. Frame timings on a profile build, checked against a budget, catch it. - **A regression test for every bug we fix**, so the agent that fixes it cannot un-fix it three prompts later. The pattern to internalize: **an agent is excellent at satisfying a check it can run, and helpless where no check exists.** Give it `flutter test` and it will iterate until green. Give it nothing and it will tell you it is confident. Our [audit findings post](https://nerdy.pro/blog/ai-code-audit-findings) covers what a codebase looks like after a year of the second option. --- ### 3. Streams, or a pyramid of flags **What the agent does.** It represents async state as loose parallel fields and then branches over them. ```dart bool _isLoading = false; String? _error; List _items = []; // build(): if (_isLoading) return const CircularProgressIndicator(); if (_error != null) return Text(_error!); if (_items.isEmpty) return const Text('Nothing here'); return ListView(/* ... */); ``` Three fields, eight representable combinations, four of them meaningless. What renders when `_isLoading` is true *and* `_error` is set? Whatever the order of the `if`s happens to be. Then the search box arrives, and the agent adds a `Timer` for debounce, a `_requestId` counter to discard stale responses, and a `mounted` check before each `setState` — a hand-rolled, subtly broken reimplementation of `switchMap`. Alongside it grows a `utils.dart` of static helpers, because the logic has nowhere structural to live. This is the specific shape: **not that agents avoid switches, but that they switch over flags they set by hand instead of over a type the compiler can check.** **What we do.** The state is a sealed hierarchy, so illegal combinations are unrepresentable, and the stream handles the timing. ```dart sealed class SearchState {} final class Idle extends SearchState {} final class Loading extends SearchState {} final class Failed extends SearchState { Failed(this.error); final AppError error; } final class Loaded extends SearchState { Loaded(this.hits); final List hits; } // build() — the compiler enforces that every case is handled return switch (state) { Idle() => const SearchHint(), Loading() => const AppSpinner(), Failed(:final error) => AppErrorView(error), Loaded(:final hits) when hits.isEmpty => const EmptyResults(), Loaded(:final hits) => HitList(hits), }; ``` And the async part — `debounceTime`, `switchMap` and `startWith` are rxdart extensions on `Stream`, not `dart:async` — where the flag version quietly races: ```dart // Debounce and cancellation are declarative. A stale query cannot win, // because switchMap already cancelled its subscription. Stream get states => _query .debounceTime(const Duration(milliseconds: 300)) .switchMap(_search) .startWith(Idle()); ``` Add a new state — `RateLimited`, say — and the compiler lists every `switch` that needs updating. In the flag version, adding a state means finding every `if` chain by hand and hoping. **Why the agent lands there.** Imperative flags are the most common pattern in the corpus and the easiest to produce one line at a time. A stream requires holding the whole state machine in your head at once, which is exactly what a context-limited generator is worst at. It is the same failure we described as [callback hell in audits](https://nerdy.pro/blog/ai-code-audit-findings) — it just wears a Dart costume. --- ### 4. Knowing the framework: `ThemeData` is not a constants file This is the one where framework knowledge shows most plainly. **What the agent does.** It creates `constants.dart`, fills it with `static const primary = Color(0xFF6750A4)`, and then writes styles inline at every call site. ```dart Text( 'Balance', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ) ``` That single line has four separate problems, and only the first is obvious: 1. **Dark mode is now a manual `if`.** `AppColors.textPrimary` is one colour. Supporting a second theme means a ternary on `Theme.of(context).brightness` at every one of the 200 sites — which is exactly what agents then write, one screen at a time. 2. **The rebrand is a 400-file diff.** A designer changing the type ramp should be a change in one file. Here it is a find-and-replace across the app, and the sites that drifted (`fontSize: 15` on two screens because that generation rounded differently) will be missed. 3. **System text scaling breaks the layout.** Hardcoded sizes composed against hardcoded paddings mean a user at 200% accessibility scale gets overflow, not reflow. 4. **`Theme.of(context)` overrides stop working.** Wrapping a subtree in a different theme — a dark section on a light screen, a branded checkout inside a neutral app — does nothing, because nothing in the subtree asks the theme anything. **What we do.** The design system lives in `ThemeData`, once, at the root. ```dart MaterialApp( theme: AppTheme.light, // ColorScheme.fromSeed + TextTheme + component themes darkTheme: AppTheme.dark, // ... ) // Every widget asks the framework instead of a constants file: Text('Balance', style: Theme.of(context).textTheme.titleMedium) ``` Component themes (`FilledButtonThemeData`, `CardThemeData`, `InputDecorationThemeData`) mean a button looks right *by default*, so the agent producing the twentieth screen cannot get it wrong even if it tries. For brand tokens Material has no slot for — a chart palette, a gradient, a semantic "positive/negative" pair in a fintech app — a `ThemeExtension` keeps them inside the same lookup instead of next to it: ```dart final class BrandColors extends ThemeExtension { const BrandColors({required this.positive, required this.negative}); final Color positive; final Color negative; // copyWith / lerp ... } // usage final brand = Theme.of(context).extension()!; ``` Dark mode then costs one more `ThemeData`, not a ternary per widget. On [Arcana](https://nerdy.pro/portfolio/arcana) the entire custom design system is expressed this way, which is why a design revision there is a diff you can read in a single review. **Why the agent lands there.** A constant is *locally* correct and immediately satisfying: the colour appears, the screen builds. Threading a design system through the framework only pays off across many screens and a second theme — and payoff-across-the-whole-app is precisely the axis a per-prompt optimizer cannot see. --- ### 5. Eyes, and a decade of shipping **What the agent does.** It builds forty screens, each one defensible on its own, that together feel like an app assembled from forty tutorials — because in a sense it was. Loading is a `CircularProgressIndicator` on the home screen, a shimmer skeleton on the feed, and a custom animated logo on the profile. Errors are a `SnackBar` here, inline red text there, and a modal `AlertDialog` on the third screen. A destructive confirm is an `AlertDialog` on one screen and a bottom sheet on the next. Empty states have three different illustrations, two different tones of voice, and one that is just the word "Empty". Some forms validate on every keystroke, some on submit, one on focus loss. Primary buttons are full-width in one flow and wrapped-to-content in another. Every one of those is a reasonable choice. The problem is that a user learns an app once. When the same action looks different in three places, they stop trusting that they know what will happen — and that cost shows up in support tickets, not in a lint report. Then there is the class of defects that only exist on a screen: - `RenderFlex overflowed by 27 pixels` on any device narrower than the agent imagined - tap targets under 44/48dp, which test fine and miss constantly on a real thumb - the keyboard covering the field being typed into, because nothing scrolls - text truncated at 200% system scale - content under the notch or behind the home indicator, because `SafeArea` was not part of the prompt - contrast that passes in light mode and fails in dark `flutter analyze` is clean for every single one. **What we do.** We decide the interaction vocabulary once — how loading looks, how errors surface, how destructive actions confirm, when forms validate, how navigation behaves, what an empty state says — and then every screen implements *that*. Ten-plus years of shipping mostly buys knowing which patterns users already have in their fingers, because thousands of other apps put them there. Familiar beats clever essentially always, and the way to check is to open the app on a real device, at a real text scale, in both themes, with a thumb. **Why the agent lands there.** It genuinely cannot see. It is generating a plausible screen from a description, with no memory of the thirty-nine screens before it and no image of the one it just produced. Local plausibility is the only target available to it. --- ### 6. ICU is not optional, and concatenation is not localization We build apps for markets where getting this wrong is visible in the first sentence of the first screen. Agents get it wrong by default. **What the agent does.** ```dart Text('$count items'); // 1 items Text('$count ${count == 1 ? "item" : "items"}'); // "clever", still broken Text('Hello, ' + name + '!'); // English word order, baked in Text('${d.day}.${d.month}.${d.year}'); // wrong half the world Text('\$${amount.toStringAsFixed(2)}'); // wrong separator, wrong position ``` The boolean-ternary plural is the instructive one, because it looks like the developer thought about it. It encodes an assumption that is true in English and false almost everywhere else: that a language has exactly two plural forms. Russian has four categories (`one`, `few`, `many`, `other`) — 1 файл, 2 файла, 5 файлов — so the ternary is wrong for most numbers on the screen. Arabic has six. Japanese has one, and pluralizing at all reads as broken. No amount of ternary nesting fixes this, because the *rule* is per-language data, not a condition you write. The same applies down the line. Concatenation bakes English syntax into the source, so a translator cannot reorder the sentence. Decimal separators, thousands separators, and currency-symbol position are locale data (`1 234,56 €` versus `$1,234.56`). Date order is locale data. And in RTL layouts, `EdgeInsets.only(left: 16)` — which agents write reflexively — puts the padding on the wrong side, where `EdgeInsetsDirectional.only(start: 16)` would have been correct. **What we do.** Messages live in ARB files with ICU MessageFormat, generated into typed accessors by `gen_l10n`: ```json { "unreadMessages": "{count, plural, =0{No new messages} one{{count} new message} other{{count} new messages}}", "@unreadMessages": { "placeholders": { "count": { "type": "int" } } } } ``` The Russian ARB for the same key carries `one`/`few`/`many`/`other` — the format has room for the language, and the framework picks the right branch from CLDR data. `NumberFormat.currency` and `DateFormat.yMMMd(locale)` handle the rest, and directional insets keep RTL honest. The call site becomes `Text(l10n.unreadMessages(count))`, which is both shorter than the ternary and correct. **Why the agent lands there.** String interpolation is the shortest path to text on screen, and it is *visibly correct* in the language the prompt was written in. The failure only appears in a locale the agent was never asked about, at a number it never tried. --- ### 7. Knowing where to simplify — and where not to The last one is judgment, and it is the hardest to hand over. **What the agent does.** It applies simplification uniformly, which produces two opposite failures in the same codebase. It **flattens complexity that was load-bearing**: ```dart try { await api.charge(order); } catch (_) { // swallowed: network failure, declined card, and idempotency conflict // are now the same event, and the retry will double-charge } ``` Retry and idempotency logic gets collapsed into a single call because "it looked redundant". A token-refresh race gets a simple `await` because the mutex "seemed unused". Distinct failure modes get merged into one `Something went wrong`, which is precisely the message that makes a payment bug unreproducible. Cancellation tokens get dropped because no test referenced them. And it **over-abstracts things that were fine**: an `AppButton` with fourteen optional named parameters and six booleans, a `BaseRepository` generic hierarchy over three endpoints, an `AbstractBaseViewModel` nobody can subclass without reading it end to end, and the inevitable `utils.dart` where forty unrelated static functions go to die. **What we do.** Complexity is a budget, and it gets spent where the domain is genuinely hard: payments, offline sync and conflict resolution, auth token refresh, idempotency, anything that touches money or identity. Those places stay explicit, verbose, and boring on purpose — each failure mode named, each retry deliberate. Everywhere else, we collapse aggressively: five near-identical list tiles become one widget, the settings screen gets no abstraction layer at all, and a repository that wraps one endpoint is just a function. **Why the agent lands there.** "Which parts of this domain will hurt someone if they are wrong?" is not information that exists in the code. It comes from the product, the regulatory context, and having been on the incident call. An agent optimizing for readable, minimal diffs treats a payment retry loop and a settings toggle as the same kind of thing, because syntactically they are. --- ### What orchestration actually means Read the seven together and the shape is clear: every one is a **whole-system property** — consistency, coverage, a state model, a design system, an interaction vocabulary, a locale model, a complexity budget. None of them can emerge one prompt at a time, because no prompt sees the whole. That is not a flaw to be prompted away; it is what the context window is. So the engineer's job moves up the stack, and it is mostly four things: **Set the invariants before the code exists.** The theme, the state model, the localization setup, the folder structure, the error taxonomy. Once those exist, "follow the existing pattern" has something to point at — and agents are genuinely good at following a pattern that is already in the repo. The first two days decide the next six months, which is why they get a phase of their own in [our development process](https://nerdy.pro/blog/flutter-app-development-process). **Build the feedback loops the agent does not have.** A strict `analysis_options.yaml` with lints promoted to errors. `flutter test` with widget and golden coverage. A frame-time budget checked on profile builds. Every one of these converts a judgment call into a check the agent can run and iterate against on its own, which is the only way its speed becomes an asset instead of a liability. **Review decisions, not lines.** Is this the same state model as the rest of the app? Is this styled from the theme? Is this the one derivation of that value? Is this string going to survive Russian? Line-by-line review of agent output does not scale and never will; decision-level review does, because the decisions are few. **Open the app.** On a device, at 200% text scale, in dark mode, in both languages, with the network throttled. This takes ten minutes and finds the entire category of defects that no static check will ever report. That is the actual difference between Flutter written by an agent and Flutter written by a team [orchestrating agents](https://nerdy.pro/blog/building-with-ai). Not the code — the code is often fine. The constraints around it. --- ### Frequently asked questions #### Can AI agents write good Flutter code? They can write good Flutter code inside a codebase that already has structure — a theme, a state model, a localization setup, and tests to iterate against. What they cannot do is create that structure, because every one of those is a whole-system property and an agent optimizes each prompt locally. Given an existing pattern to follow, agent output is genuinely strong. Given an empty repository and no constraints, it produces forty screens that each work and do not add up to one app. #### Why does AI-generated Flutter code look inconsistent across screens? Because the agent has no memory of the screens it built earlier and no way to see the app it is building. Each screen is generated as a plausible standalone answer, so loading indicators, error handling, empty states, confirmation dialogs and form validation timing all differ from screen to screen. Every individual choice is defensible; the aggregate reads as an app assembled from unrelated tutorials, and users notice because they only learn an app once. #### Why do AI agents hardcode colors and text styles instead of using ThemeData? A constant is locally correct and immediately satisfying — the color appears and the screen builds. Wiring a design system through ThemeData only pays off across many screens and a second theme, which is exactly the horizon a per-prompt optimizer cannot see. The cost arrives later: dark mode becomes a manual condition at every call site, a rebrand becomes a several-hundred-file diff, and system text scaling breaks the layout because sizes are fixed numbers rather than a type ramp. #### What is wrong with using string interpolation for plurals in Flutter? It encodes an assumption that is true in English and false almost everywhere else: that a language has exactly two plural forms. Russian has four plural categories, Arabic has six, and Japanese has one. A condition such as count equals one cannot express any of them. ICU MessageFormat in ARB files handles plural categories per language from CLDR data, and the same applies to number formatting, currency symbol placement, date order and right-to-left layout, none of which survive string concatenation. #### Do AI agents write tests for Flutter apps? Not unless asked. Generating a feature and generating its tests are two separate requests and the second one usually never happens, so code review becomes the only quality gate at exactly the moment agents multiply the volume of code arriving at it. The highest-leverage fix for UI work is golden tests, because a golden is the closest thing to giving an agent eyes: it turns questions like whether a redesign broke the empty state in dark mode into a check that runs in continuous integration in seconds. #### Why do AI agents avoid streams in Flutter? Imperative flags are the most common pattern in training data and the easiest to produce one line at a time, while a stream requires holding an entire state machine in mind at once. So agents write parallel boolean and nullable fields and branch over them, which makes illegal combinations representable and stale responses possible. Modelling the same thing as a sealed state class consumed from a stream makes the compiler enforce exhaustiveness and makes debouncing and cancellation declarative rather than hand-rolled. #### Should we rewrite an AI-built Flutter app? Usually not. The features work and the product is real; what is missing is the connective tissue — a single source of truth for derived state, a test and golden layer, a proper state model for complex async, a theme instead of constants, a consistent interaction vocabulary, and real localization. All of that is fixable in place, and doing it in place is far faster than a rebuild. A focused audit identifies which of the seven is costing the most and in what order to fix them. ### Working on a Flutter app an agent built If you recognize your codebase in this list, it is not a sign that using AI was a mistake — it is what unsupervised agent output looks like in Flutter, every time, and it is fixable without a rewrite. We build Flutter this way for a living: agents write a lot of the code, and our engineers own the seven decisions above. Have a look at [how we do Flutter app development](https://nerdy.pro/services/flutter-app-development), run an existing codebase through an [AI code audit](https://nerdy.pro/services/ai-code-audit), or [book a free assessment](https://nerdy.pro/contact) and we will tell you which of these seven is costing you the most. ## How We Built a Real-Time Fintech App with Flutter and Go https://nerdy.pro/blog/extraetf-realtime-fintech-flutter-go We built [ExtraETF](https://nerdy.pro/portfolio/extraetf), a real-time ETF and stock investing app, in Flutter with a Go backend for the live market-data layer. It shipped in 2022 for Isarvest GmbH and it is still in the App Store and Google Play. This post is the engineering deep-dive the portfolio page cannot hold: how the streaming layer actually works, how we build financial charts in Flutter, and — because honesty is the whole point — where the stack fought us and what we would change in 2026. A note up front on numbers. We do not publish our clients' business metrics — user counts, revenue, retention — and we have [written about why vague growth claims are worthless](https://nerdy.pro/blog/mobile-cost-reduction-case-studies). The figures here are about the *system* — code reuse, buffer sizing, update cadence — not the client's market. ### The short version ExtraETF is a chart-heavy investing app that streams live prices to many clients at once. Two problems dominated the build. The first was **real-time market data fan-out**: getting one upstream price feed to every client subscribed to a given instrument, at a bounded update rate, without falling over when connections drop. The second was **rendering real financial charts in Flutter** — axes, gridlines, multiple series, a scrub crosshair — efficiently enough to stay inside the frame budget on a phone. The architecture is three layers: an upstream market-data source, a **Go WebSocket service** that manages connections and fans updates out with backpressure, and **Flutter clients** that consume the stream and render the UI. Go handles the concurrency; Flutter handles one codebase across iOS and Android with full control over rendering. That is the story in three sentences; the rest is how each layer earns its place. ### What a real-time investing app actually demands Strip away the branding and a live investing app is a hard systems problem wearing a finance costume. Four requirements set the bar: - **Live prices, pushed not polled.** Quotes change through the trading day. Polling a REST endpoint every few seconds is both too slow for the user and too expensive at scale. You need a push channel. - **Many clients, one source.** The upstream feed is read once; the price for a given instrument has to reach every client currently watching it. That is a fan-out problem, and fan-out is where naive implementations melt. - **Dense, interactive charts.** A price line, an area fill, multiple series and comparison overlays, a scrub crosshair — all on a 6-inch screen, all inside the frame budget. - **The fintech reliability bar.** Users forgive a social app that drops a frame. They do not forgive a money app that shows a stale price, silently loses its connection, or janks while they read a chart. Reconnection, resubscription, and correctness are not polish; they are the product. Everything below follows from those four. ### Why Flutter and Go **Flutter for the client.** We needed one team shipping the same feature to iOS and Android on an aggressive timeline, and we needed the *option to own the pixels* — a real financial chart is custom rendering, not a stock UI widget. Flutter gives you both: a single Dart codebase and a canvas you can draw on directly. Code reuse across iOS and Android came in around 99% — the Dart codebase is shared wholesale, with only a few hundred lines of platform-specific native glue for billing, OAuth, and push. If you want the long-form version of that tradeoff, we wrote [Flutter vs native in 2026](https://nerdy.pro/blog/flutter-vs-native-2026). **Go for the WebSocket layer.** The streaming service is a concurrency problem before it is anything else. Go's model — a couple of lightweight goroutines per connection plus a single fan-out loop — fits that shape almost perfectly: a blocked goroutine costs kilobytes, not an OS thread, and channels make the fan-out logic readable instead of a callback maze. The per-connection cost is a parked goroutine or two and a buffer, so the honest ceiling is file descriptors, not CPU. The tradeoffs, honestly. Go's error handling is verbose, and a WebSocket service is *all* edge cases — half-open connections, slow consumers, partial writes — so there is a lot of `if err != nil`. Flutter in 2022 meant fighting the framework on a few rendering details and accepting that some native SDKs (payments especially) had rougher plugin support than a native app would. Neither was a dealbreaker. Both were real. ### The architecture The system is a straight pipeline with one service doing the hard part in the middle — a conventional Flutter WebSocket architecture. It has three layers, left to right: - **The upstream market-data source** — the raw price feed, read once. - **The Go WebSocket service** — the fan-out layer in the middle, and the only component that talks to the upstream feed. Every client connects here, never to the source directly. - **The Flutter clients** on iOS and Android — each holds a single WebSocket, turns the incoming stream into UI state, and renders it in live price cells and the chart layer. Prices flow left to right — the source into the service, the service out to the clients — while subscriptions flow the other way: each client tells the service which symbols are on screen, and only updates for those symbols come back down. Three behaviors make it robust, and they live almost entirely in the service in the middle: - **Subscription routing.** Clients do not receive the whole firehose. Each client subscribes to the symbols on screen, the service keeps a per-symbol set of subscribers, and an update is delivered only to the clients that asked for that symbol. On subscribe, the service immediately replays the last known quote so a fresh screen is never blank. - **Reconnection and resubscription.** Mobile connections die constantly — tunnels, backgrounding, network switches. The client treats a dropped socket as normal, reconnects after a short delay, and *replays its current subscriptions* so the server rebuilds that client's routing state. - **Not rebuilding the world on every tick.** A price cell updates by rebuilding just that one widget; the chart is not driven off the raw tick stream at all. More on both below. ### Hard problem #1 — real-time at scale At the core is the classic hub: a single goroutine owns the fan-out. Each connection has its own reader and writer goroutine — the writer drains a buffered channel and writes to the socket — and the hub keeps, per instrument, the set of clients subscribed to it. The naive alternative, writing synchronously to every subscriber inside the broadcast, deadlocks the moment one client's socket is slow, because every other client waits behind it. That is the first thing that breaks. The fix is the standard-but-easy-to-get-wrong one: give every client a **buffered send channel** and make the broadcast a **non-blocking send**. If a client's buffer is full, it cannot keep up — and for live prices the right move is not to block the fan-out. ```go // Broadcast an update to everyone subscribed to a symbol. // A slow client never blocks the others: if its buffer is full, // we drop the whole connection rather than stall the fan-out. func (h *Hub) broadcast(symbol string, update Update) { for _, c := range h.subscribers[symbol] { select { case c.send <- update: // buffered channel, non-blocking default: // Buffer full → this client cannot keep up. Drop it; // it will reconnect and resubscribe with a clean buffer. h.drop(c) } } } ``` Two decisions turned this from "works in a demo" into "works at the market open": - **Backpressure is a connection state, not a per-message one.** The send buffer is deliberately deep — a stalled client can fall tens of thousands of messages behind before anything gives — so a brief hiccup is absorbed, not punished. But a client that keeps overflowing is *disconnected*, not nursed along on stale data. It reconnects clean. The buffer trades memory for tolerance; that is a knob, and we turned it up. - **Coalescing happens upstream, before the socket layer.** This is the honest part most "real-time" posts skip. Updates are conflated *before* the fan-out — roughly one update per instrument per second, latest-value-wins — so a busy symbol never floods anyone. That means this is **not** sub-100ms tick-by-tick streaming; it is a steady **~1-second cadence**. Which is exactly what a human watching a price needs, and what keeps client CPU and bandwidth sane. The payoff of the goroutine model shows up here: the cost of an idle subscriber is a parked goroutine and a buffer, so a single node holds far more idle connections than a thread-per-connection server ever could. The real constraint has always been socket and file-descriptor limits, not the Go runtime — so you provision the descriptor ceiling for that. In production a single node carries thousands of concurrent connections with an order of magnitude of headroom to spare. ### Hard problem #2 — rendering financial charts in Flutter ExtraETF is chart-heavy, and rendering a real financial chart in Flutter is its own discipline. Here is how we build them. For anything past a lightweight sparkline, we build the chart as a **custom `RenderObject`**, not a `CustomPainter`. A `RenderBox` with a custom `Element` lets you treat the axis labels, the legend, and the per-series markers as real child render objects with their own layout — far cleaner than hand-placing everything onto one flat canvas, and it gives you precise control over what happens at layout time versus paint time. That split is the whole performance story. The techniques that keep it cheap: - **Do the geometry once, at layout.** The `Path` and `Paint` objects for each series are built in `layout()` and cached; `paint()` just draws the cached paths. The expensive work — walking the series, projecting points — happens when the data or the box changes, not on every frame. - **Memoize the scale.** The mapping from data space to pixels (and the chart rectangle) is cached and recomputed only when its inputs change, behind equality-guarded setters. - **Gate repaints at the source.** Instead of a `shouldRepaint` heuristic, the render object's property setters compare first — a `listEquals` on the series list — and only call `markNeedsLayout` / `markNeedsPaint` when something visible actually changed. ```dart set series(List value) { if (listEquals(value, _series)) return; // nothing visible changed _series = value; markNeedsLayout(); // paths, scale and labels rebuilt in layout(), then one repaint } ``` - **Keep heavy data prep off the UI thread.** Filtering a long benchmark series down to the selected range runs in a `compute()` isolate, so the main thread never stalls preparing what the chart will draw. The one gesture that matters on a financial chart is the **scrub crosshair**. We attach it with a `HorizontalDragGestureRecognizer` — chosen deliberately so the chart wins the gesture arena against an ancestor `PageView`/`TabBar` swipe while vertical scrolling still works. A scrub moves the marker and rebuilds only the small legend/indicator overlay, with a haptic tick; it never calls `setState` on the tree. Where it fought us: the scrub path is the interesting cost. Moving the crosshair invalidates *both* paint and layout, so an active scrub re-runs `layout()` and rebuilds those cached paths — the cache that shields ordinary frames does not shield a scrub. That is the kind of thing you only find with the frame budget open in front of you, and it is where the next round of optimization goes. One deliberate non-feature: there is no pinch-zoom or pan-to-scroll a window. Changing the time range **refetches data and produces a new series**; it is not an in-chart transform. That keeps the chart stateless with respect to the viewport and leaves the data layer as the single source of truth — a real tradeoff, made on purpose. None of this is about chasing a benchmark. The frame budget is **16.6ms**, and a full financial chart — axes, gridlines, several series, a legend, markers — is a lot to fit into it. That is exactly why the geometry work lives in `layout()` and `paint()` stays cheap. Separately, the live price *numbers* on screen do update on every tick — by rebuilding just that one widget, a value-listenable wrapped around the price cell, so a moving price never rebuilds the chart or the page. One fintech-specific rule to close, because it bites everyone eventually: **never do the money math in floating point.** Prices, percentage changes, and portfolio sums belong in integer minor units or a decimal type, not a `double`. We wrote the [full explanation of why `0.1 + 0.2 ≠ 0.3` matters for money](https://nerdy.pro/blog/floating-point-and-money) — in a finance app it is not academic. ### The platform integration layer The streaming service and the charts are the interesting engineering. The platform layer is where the *schedule* actually goes, because every item here is a native SDK with its own edge cases that a cross-platform framework cannot abstract away: - **Sign-in with Apple and Google OAuth.** The happy path is quick; the edge cases are not. Apple hands you the user's name and email *only on the very first authorization* — miss it and it is gone — so you capture and persist it then, or never. Linking the two providers to the same human is its own small design problem. - **Push notifications for price alerts.** Delivering a "your alert triggered" push means token lifecycle management, permission prompts timed so users actually say yes, and a payload design that stays useful when the app is backgrounded or killed. - **In-app purchases and subscription entitlements.** This is the one that costs the most time in any fintech build. Tiers, free trials, purchase restoration across a user's devices, and the reconciliation between the store's receipt and your own entitlement state. Store rules differ between Apple and Google, and neither is forgiving. Flutter does not make this harder than native — but it does not make it meaningfully easier either. - **Deep linking into specific equities.** A link should open the app directly on the right instrument, including when the app was not installed at tap time. Deferred deep linking is a genuinely tricky corner that got harder after Firebase Dynamic Links shut down; we wrote up [how we handle it now with App Clips and the Play Install Referrer](https://nerdy.pro/blog/deferred-deep-linking-app-clips-install-referrer). The app ships across nearly 70 screens — onboarding, live quotes, news, watchlists, multi-timeframe charting, and subscription management — all from the one Flutter codebase. ### What we would do differently in 2026 A 2022 build reviewed in 2026 is a useful honesty test. What has aged well: the shape. Go for the fan-out layer and Flutter for the client is still exactly what we would choose today — the goroutine model and a single custom-rendering client have only gotten stronger arguments in their favor. What we would change: - **Move the charts fully native.** Building the chart as a custom `RenderObject` — rather than leaning on heavier off-the-shelf options — is where we have landed, and it is the direction we are taking our charting. Impeller makes that bet stronger: custom-rendered charts are far more predictable now than on the old Skia path, with less shader-compilation stutter on first paint. A chart-heavy app started today leans on that from day one. - **Reach for a proven realtime layer before hand-rolling.** Our Go service is not complex, but connection management, reconnection, and fan-out are *solved* problems. For a new build we would seriously weigh a managed or battle-tested realtime layer and spend the saved time on the domain logic — dropping to a custom Go service only if the economics or latency requirements demanded it. - **Type the client-server contract end to end.** The stream messages were hand-serialized. Today we would define the schema once and generate both the Go and Dart types from it, so a field rename cannot silently desync the two ends. None of that is a knock on the original build. It shipped, it is still live, and the core decisions held up. It is just what four years of ecosystem progress buys you. ### FAQ #### Can Flutter handle real-time financial data? Yes. Flutter handles a live market-data stream well because the network and decoding work runs off the UI thread and only the widgets bound to a changed value need to update. The constraint is rarely Flutter itself; it is how you deliver updates. Conflate ticks upstream to a bounded cadence so the client is not flooded, and update the smallest possible widget subtree per change so the UI stays within its frame budget. #### Is Flutter a good choice for fintech apps? Flutter is a strong fit for most fintech apps: one codebase for iOS and Android, full control over custom chart rendering, and a mature plugin ecosystem for OAuth, push notifications, and in-app purchases. It is a weaker fit only when the product depends on platform-exclusive hardware or on shipping a brand-new OS feature on the day it launches. #### How do you render performant charts in Flutter? For a lightweight chart a CustomPainter is enough. For a full financial chart we build a custom RenderObject: it lets you manage axes, gridlines, the legend, and per-series markers as child render objects, build and cache the Paths and Paints once at layout time, memoize the scale, and repaint only when the data actually changes. Push heavy data preparation off the main thread with compute(). A time-range change is a data refetch that yields a new series, not an in-chart pan or zoom transform. #### Why use Go for a WebSocket service? Go maps each connection to a couple of lightweight goroutines, so a single instance can hold many thousands of concurrent WebSocket connections with a small, predictable memory footprint bounded by file-descriptor limits rather than CPU. Its channel-based concurrency makes fan-out, broadcasting one price update to many subscribers, straightforward to write correctly, including the backpressure handling that slow clients require. #### What is a WebSocket fan-out service? A WebSocket fan-out service is a server that keeps one persistent connection open per client and pushes the same event to every subscribed client as it arrives, instead of each client polling for updates. It centralizes connection management, subscription tracking, and rate control, so an upstream data source is read once and broadcast to many clients. #### Can Flutter apps handle subscriptions and in-app purchases? Yes. Flutter apps integrate App Store and Google Play billing through official plugins, covering subscription tiers, free trials, purchase restoration, and server-side receipt validation. The hard part is not Flutter but the store rules and entitlement edge cases, and those are the same whether the app is built in Flutter or natively. ### Building something real-time or fintech? That is the work we do. If you are streaming live data to mobile clients, rendering something heavier than a list, or wiring up subscriptions and OAuth without the sharp edges, we have shipped it. See the [ExtraETF project page](https://nerdy.pro/portfolio/extraetf) for screenshots and details, browse [our open-source Flutter packages](https://nerdy.pro/open-source), or read more about our [Flutter app development](https://nerdy.pro/services/flutter-app-development) service and how we approach [fintech app development](https://nerdy.pro/services/flutter-app-development/fintech) specifically. Have a project in mind? [Get in touch](https://nerdy.pro/contact) — tell us the hard part, and we will tell you honestly whether Flutter and Go are the right call for it. ## How We Ship 20+ Branded Apps From One Flutter Codebase — and Survive Apple 4.2.6 https://nerdy.pro/blog/white-label-app-platform-flutter **TL;DR.** Yes — you can ship many branded apps from one codebase and pass App Store review, but only if each app is *genuinely differentiated in content and behavior*, not just skinned with a new logo. The decision rule: **if two of your apps show the same screens with the same data and only the branding changes, Apple will treat them as duplicates and reject them under guideline 4.2.6; if each app authenticates as its own tenant and pulls its own content, catalog, features, and config from the server, they are functionally distinct products that pass.** The trap that catches template mills is treating white-label as a reskin. The thing that makes it work is a client-to-server layer that makes each app substantively different at runtime. Everything below is how we build that — the architecture, the submission strategy, and the economics of app number twenty costing a fraction of app number one. We run a Flutter-first agency, and one of the platforms we built runs **15+ live branded apps from a single codebase** ([cashback & loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform)). This is the honest version of how that works, including the part where a lazy version of it gets you pulled from the store. ### Who this is for You're not buying *an* app. You're buying *apps* — plural, and you need a lot of them. - **Agencies** reselling mobile apps to their own clients — you want to onboard a new client and hand them a branded app without running a fresh build project each time. - **Franchise networks** where every location or region needs its own store presence under its own brand, but the product underneath is one product. - **Multi-brand operators** running a portfolio of brands that each need a customer-facing app. - **Course, coaching, and community platforms** shipping a per-creator or per-cohort app. - **Event organizers** who spin up an app per event or per venue and retire it after. The common shape is "I need 10, or 50, or 500 apps, and I can't afford to build and maintain each one from scratch." If that's you, you've probably been pitched "white-label" before — and if you've been burned, it was almost certainly because someone sold you a reskin and Apple noticed. ### Why most white-label apps get rejected: the 4.2.6 trap **App Store Review Guideline 4.2.6 is the rule Apple uses to remove apps "created from a commercialized template or app generation service" unless they're submitted by the business the app is *for*, and it's the rule they lean on to reject apps that are duplicates or reskins of each other.** The intent is simple: the store shouldn't fill up with hundreds of near-identical apps that add no standalone value. Apple wants each app on the store to be a real product, not spam generated from a factory. Here's why the typical white-label pitch walks straight into it. A template mill takes one binary, swaps the logo, changes the color palette, changes the app name, and submits forty copies. Every one of those apps: - shows the **same screens** in the same order, - displays the **same content and data**, - exposes the **same features**, - and often ships with **near-identical screenshots and metadata**. To a reviewer — and increasingly to Apple's automated tooling — that's forty copies of one app. The differentiation is cosmetic. Guideline 4.2.6 exists precisely to reject that, and the rejections are brutal: apps pulled after months live, developer accounts flagged, whole fleets held in review. The scar most buyers arrive with is exactly this — "we shipped six branded apps, Apple approved four, then retroactively rejected the batch as duplicates." The lesson people take from that is usually wrong. They conclude "you can't ship many apps from one codebase." You can. The problem was never the shared codebase. **The problem was that the apps were the same product wearing different hats.** ### The differentiation that actually passes review This is the whole point, so let me be precise about it. There are two layers of differentiation. Almost everyone does the first. The second is what separates a real platform from a template mill. **Layer 1 — visual white-labeling (necessary, not sufficient).** Each app gets its own theme (colors, typography, logo, splash, light/dark brightness), its own app name, its own bundle identifier / application ID, its own icon, and its own store listing. This is table stakes. It makes the apps *look* different. On its own, it is exactly what 4.2.6 rejects. **Layer 2 — the client-to-server layer (the actual moat).** This is where we don't just white-label the *presentation* — we differentiate the *content and behavior*. Every branded app, at runtime, **authenticates to the backend as its own tenant** and pulls its own world down from the server: - its own **content** (the actual screens' data, copy, media, feed), - its own **catalog** (products, listings, courses, offers — whatever the app is *about*), - its own **data** (a customer in brand A's app never sees brand B's data; brand context is resolved per request), - its own **feature set** (per-tenant feature flags turn whole modules on or off), and - its own **remote configuration** (behavior, campaigns, rollout — all server-driven). The concrete before/after: > **A reskin** shows the same catalog, the same feed, the same features, with a new logo on top. > > **Our apps** pull a *different* catalog, a *different* feed, and can expose *different* features per client — two apps built from the same binary blueprint are genuinely different products the moment they boot and talk to the server. That substantive, server-driven differentiation is the reason each app is a real standalone app in Apple's eyes. A reviewer opening two of our apps doesn't see the same app twice — they see two products with different content and, often, different capabilities. That is what 4.2.6 asks for, and it's what pure reskins can't fake, because a reskin has nothing different to show. > **The one sentence to remember** > > Visual theming makes an app *look* different. The client-to-server layer makes it *be* different. Apple's 4.2.6 rejects the first when it's all you've got, and accepts apps that genuinely differ in content and behavior — so the server layer isn't a nice-to-have, it's the thing that gets you approved. ### The architecture Here's the pipeline end to end, stage by stage — from a manager creating a brand to a signed app landing in the right store account. 1. **Admin panel (Nuxt).** A manager creates a new brand — name, assets, theme and brightness, feature selection, store metadata — and submitting the form generates that brand's configuration. No engineer in the loop. 2. **Backend / multi-tenant API.** The configuration lands in the backend as a new tenant: its theme tokens, its catalog and content and data, its feature flags, and its own per-tenant credentials. This is the tenant's "world," and it's the thing the app will pull down later. 3. **Build pipeline (build time).** Generating the config triggers a build. Custom Dart tooling applies the brand's *identity* to the Flutter binary — theme, app name, bundle identifier, icons — by rewriting the Android and iOS build configuration deterministically from the config (no hand-editing `build.gradle` or `Info.plist` per app). [Fastlane](https://fastlane.tools) then signs the iOS and Android binaries. 4. **The branded app (runtime).** Every app is the same Flutter binary blueprint. On launch, it authenticates to the backend *as its own tenant* and pulls its own content, catalog, data, feature set, and remote config — so app #17 shows a different world than app #3, even though they're the same code. 5. **Multi-publisher deployment.** Fastlane ships each signed app to the right store target — App Store account A, App Store account B, Google Play, and so on — so brands can live under separate publisher accounts rather than one. Read that as two flows meeting at the app. **At build time** (stages 1–3), the pipeline bakes the brand's *identity* into the binary and signs it — that's what makes it a distinct store listing. **At runtime** (stage 4), the app authenticates as its tenant and pulls its *content and behavior* from the backend — that's what makes it a distinct product. Build time makes it a distinct listing; runtime makes it a distinct product. The split between what's fixed at build time and what's resolved at runtime is the whole design, so here it is as a table: | Layer | Differentiated at | What varies per app | Why it matters for 4.2.6 | | --- | --- | --- | --- | | **Bundle identity** | Build time | App name, bundle ID / application ID, icons, splash | Each app is its own store listing under its own publisher | | **Theme & branding** | Build + runtime | Colors, typography, logo, brightness | Visual white-label — necessary, not sufficient on its own | | **Tenant authentication** | Runtime | Per-tenant credentials, brand context | Each app is its own authenticated client on the server | | **Content & catalog** | Runtime | Screen data, listings, feed, media, copy | Genuinely different content = the standalone value Apple wants | | **Feature set** | Runtime | Enabled modules, per-tenant flags | Apps can *behave* differently, not just look different | | **Remote config** | Runtime | Behavior, campaigns, staged rollout | Ongoing divergence with no rebuild per change | Flutter is what makes this economical. Building this fleet natively would mean maintaining Swift *and* Kotlin codebases multiplied by every brand — an impossible burden. Flutter collapses it to **one Dart codebase, one team, every platform and brand**, which is the same foundation as our [Flutter app development](https://nerdy.pro/services/flutter-app-development) work, scaled from one app to a fleet. Because it renders every pixel itself, a brand theme applies identically across devices, and a fix shipped once lands in all N apps on the next release. ### Publisher accounts and submission strategy Passing 4.2.6 isn't only about the code. The *submission* has to look like N real products too, because that's the surface a reviewer actually sees. The single biggest red flag you can wave at Apple is **one developer account publishing forty visually similar apps**. That pattern is the signature of a template mill, and it invites exactly the duplicate-app scrutiny you're trying to avoid. So the account strategy is part of the architecture, not an afterthought: - **Separate publisher accounts per brand or client** is the safest model, and often the correct one anyway — when the app is *for* your client (an agency reselling to them, a franchisee, an independent partner brand), it should be published under *their* account. Guideline 4.2.6 explicitly favors this: template-derived apps are acceptable when submitted by the business the app serves. - **Single account with genuine differentiation** can work for one company's own sub-brands, but the bar on content and metadata differentiation is higher, because everything sits under one publisher. - **Metadata and screenshots must differ per app** — real, brand-specific descriptions and screenshots taken from *that brand's actual content*, not one screenshot set with the logo swapped. Duplicated marketing assets get flagged even when the app itself is differentiated. Our pipeline supports all three publishing models and mixes them per brand, because the right answer depends on your partner agreements and who the app is legally *for* — we walk through the tradeoffs in detail on the [white-label app development](https://nerdy.pro/services/whitelabel-app-development) service page. ### Build vs buy vs license You have three honest options, and we'll tell you which one fits even when it isn't us. **Build it yourself.** You own everything and it's tuned to your exact model. But you're funding the platform — the config layer, the admin panel, the multi-tenant backend, the Dart build tooling, the Fastlane pipeline — before app number one ships, and you're taking on the 4.2.6 risk with no scar tissue. This makes sense when a mobile platform *is* your business and you'll run an engineering team around it for years. If you want to build it but need Flutter horsepower to do it, that's what [team augmentation](https://nerdy.pro/services/team-augmentation) is for — our engineers in your repo, the platform stays yours. **Buy a template mill.** Cheapest sticker price, fastest demo, and the highest chance of a 4.2.6 rejection, because reskins are the exact thing the guideline targets. If the pitch is "we change your logo and colors and submit it," you're buying the rejection, not avoiding it. There's a place for template builders — internal tools, enterprise distribution, prototypes — but not a public multi-brand fleet on the App Store. **License or partner with a team that's shipped this.** You get the platform architecture and the submission playbook without funding the R&D or absorbing the rejection risk first-hand. The tradeoff is a partner in the loop. This is the model most agencies and multi-brand operators should want — you're buying a proven pipeline, not discovering the failure modes yourself. There's no universally right answer. There *is* a wrong one: buying a reskin and finding out about 4.2.6 from a rejection email. ### The economics: why app 2…N is cheap The reason this model exists is the cost curve, so here's the honest shape of it. **App number one carries the platform.** The first app isn't really "an app" — it's the codebase, the theming and feature-flag system, the multi-tenant backend, the admin panel, and the automated build-and-publish pipeline. That's the investment, and it's front-loaded. As a rough anchor, a white-label platform build lands roughly in the range of a serious custom app project and scales with fleet size and feature depth — the tiers are on the [service page](https://nerdy.pro/services/whitelabel-app-development), and the general cost mechanics are in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026). **Apps 2 through N are dramatically cheaper**, because they share everything that cost money. A new branded app is, mechanically, a *configuration*: brand assets, a theme, a feature selection, store metadata, and a publisher account. No new build project, no forked codebase, no second round of architecture. The marginal cost of app N approaches the cost of *filling in a form and preparing store assets* — hours, not a rebuild. A rough model, without inventing your numbers: - **Total cost of N apps ≈ platform cost + (N × per-brand config cost).** - The platform cost is fixed and paid once. The per-brand cost is small and roughly flat. - So your **average cost per app falls as N grows** — the more brands you ship, the closer the average gets to the marginal config cost. On top of that curve sits Flutter's structural saving: one codebase for iOS and Android is **~30–40% cheaper than building two separate native apps**, and that saving *compounds across the fleet* — you're not paying it once, you're avoiding it N times. For our own numbers: we've shipped **20+ branded apps** from our white-label codebases, and on [one platform](https://nerdy.pro/portfolio/cashback-loyalty-platform) alone the fleet passed **15+ live apps** from a single codebase, where launching the next brand is close to free. ### FAQ #### What is a white-label mobile app? A white-label mobile app is a single application that is rebranded and republished as multiple separate, independently branded apps. Each one has its own name, logo, colors, and App Store or Google Play listing, but they all run on the same shared codebase. Done properly, each branded app also pulls its own content, catalog, and features from a server at runtime, so the apps differ in what they show and do — not just how they look. #### What is App Store guideline 4.2.6? App Store Review Guideline 4.2.6 is Apple's rule against apps created from a commercialized template or app generation service, and against apps that are duplicates or reskins of each other. Its intent is to keep the store free of spam and near-identical apps that add no standalone value. Template-derived apps are allowed when they are submitted by the business the app is actually for — for example, published under a client's or franchisee's own developer account rather than mass-produced under one publisher. #### How do you avoid a 4.2.6 rejection? You make each app genuinely different in content and behavior, not just appearance. Each branded app authenticates to the backend as its own tenant and pulls its own content, catalog, data, feature set, and configuration from the server, so two apps built from the same codebase show different things and can behave differently. You also differentiate the submission — separate publisher accounts per brand or client where appropriate, and unique, brand-specific screenshots and metadata for each app. Visual theming alone is exactly what 4.2.6 rejects; substantive, server-driven differentiation is what passes. No one can guarantee approval, but this is what materially separates apps that pass from reskins that don't. #### Can you really build many apps from one codebase? Yes. One Flutter codebase can become any number of branded apps, because what differs between them — branding, theme, content, catalog, feature flags, and store metadata — lives in configuration outside the code, and each app resolves its own configuration and content from the server at runtime. A bug fixed or a feature shipped once lands in every app on the next release. One platform we built runs 15+ live branded apps from a single codebase, and the marginal cost of the next app is close to zero. #### How much does a white-label app platform cost? The first app carries the platform cost — the shared codebase, the theming and feature-flag system, the multi-tenant backend, the admin panel, and the automated build-and-publish pipeline — so it costs like a serious custom app build. Every app after that is dramatically cheaper, because a new brand is a configuration rather than a new project, and its marginal cost approaches the cost of preparing brand assets and store metadata. Your average cost per app falls as you launch more brands. Flutter's single codebase also runs about 30–40% cheaper than building separate native iOS and Android apps, a saving that compounds across the whole fleet. #### How is this different from a template app builder? A template app builder produces reskins: the same app with a new logo and colors, showing the same screens, data, and features. That is precisely what App Store guideline 4.2.6 rejects. Our approach adds a client-to-server layer on top of the shared codebase, so every app authenticates as its own tenant and pulls genuinely different content, catalog, data, and features from the server. The apps are functionally distinct products, not cosmetic copies — which is why they stand up to review where template-mill apps get pulled. ### Talk to us about a white-label build If you need many apps and not one, the interesting question isn't "can Flutter do it" — it's "will these apps survive review, and what does the pipeline actually take." We've built the architecture, hit the 4.2.6 edge cases, and shipped fleets that stayed live. - **Book a platform demo** — see the admin panel spin up a new branded app and the pipeline sign and publish it: [talk to us](https://nerdy.pro/contact). - **Go deeper on the offering** — architecture, publishing models, and pricing tiers on the [white-label app development](https://nerdy.pro/services/whitelabel-app-development) service page. - **Building it yourself?** We'll put Flutter engineers in your repo via [team augmentation](https://nerdy.pro/services/team-augmentation) and leave the platform in your hands. Tell us how many brands you're planning and who the apps are *for*, and we'll give you a straight read on the model, the submission strategy, and the cost curve — not a template-mill pitch. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that builds white-label platforms and ships branded app fleets across fintech, retail, and loyalty.* ## Hire Flutter Developers in 2026: In-House vs Agency vs Staff Augmentation https://nerdy.pro/blog/hire-flutter-developers-2026 **TL;DR.** There are three ways to hire Flutter talent in 2026, and the right one depends on what you're actually buying. **Hire in-house when Flutter is core to your product for years and you want to own the code and the knowledge. Hire an agency when you need a defined product shipped fast with the least management overhead. Use staff augmentation when you already have a team that can manage engineers and just need more Flutter capacity.** The one-line decision rule: *if the mobile app is the business, build in-house; if you need it built, hire an agency; if you need it built faster, augment your team.* Everything below is the detail behind that sentence — real 2026 rate ranges, hidden costs, and a checklist for vetting whoever you hire. We run a Flutter-first agency, we place engineers into client teams, and we've watched all three models succeed and fail up close. This post is the honest version, including the parts where the answer is "don't hire us." ### The three models at a glance | | In-house | Agency | Staff augmentation | | --- | --- | --- | --- | | **Upfront cost** | Highest — recruiting fees, salaries, benefits, equipment before a line ships | Medium — scoped project fee, predictable | Low — pay per engineer, per month, start/stop fast | | **Time-to-productive** | Slowest — 2–4 months to hire, then ramp | Fastest for a whole product — team is pre-formed | Fast — days to a couple of weeks per engineer | | **Management overhead** | High — you own hiring, retention, career growth, sprint planning | Lowest — the agency manages delivery | Medium — you manage the engineers day-to-day | | **Flexibility / scaling** | Low — hiring and firing are slow and expensive | Medium — scope up/down at contract boundaries | Highest — add or drop engineers month to month | | **Code ownership & knowledge retention** | Best — knowledge stays in the building | Weakest by default — knowledge can walk out at handoff | Strong — code and context live in *your* repo and team | | **Best-fit scenario** | Flutter is core product, multi-year roadmap | Defined product, fixed deadline, minimal internal capacity | Existing team needs more Flutter throughput now | | **Biggest risk** | Slow, expensive, and you own a bad hire | Lock-in and a knowledge cliff at handoff | You still have to manage them — augmentation isn't autopilot | ### What does it actually cost to hire a Flutter developer in 2026? Cost depends more on *where* and *how* you hire than on the framework. Here are the ranges we see in the market in 2026. **Agency (blended hourly, by region).** This is the all-in rate for a team that includes engineering, project management, and usually design: - **US-based agencies:** $150 – $250/hour - **Western Europe (UK, Germany, Netherlands):** $100 – $180/hour - **Eastern Europe:** $50 – $95/hour - **South/Southeast Asia:** $25 – $55/hour (wider quality variance) **In-house (annual salary, typical 2026 market ranges).** A senior Flutter engineer costs roughly $120k–$180k+ in the US, €60k–€100k in Western Europe, and $35k–$70k in Eastern Europe. Then multiply by **1.25–1.4×** for the fully loaded cost — payroll taxes, benefits, equipment, software, and office or remote overhead. A $150k salary is a ~$190k cost. On top of that, budget a one-time recruiting cost of 15–25% of first-year salary if you use an agency or in-house recruiter. **Staff augmentation (per engineer, monthly).** Augmentation typically lands **10–30% below** a full agency project rate for the same seniority, because you're buying the engineer's time, not the whole delivery wrapper (PM, QA lead, design). You avoid recruiting fees and long-term employment liability entirely — you pay a monthly rate and can stop when the work is done. One number holds across all three models: **Flutter cuts build cost roughly 30–40% versus two separate native apps**, because one codebase covers iOS, Android, and often web. That saving is the same whoever you hire — it's a property of the framework, not the hiring model. If you're pricing an actual build, we broke the whole thing down by tier in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026). ### When does hiring in-house make sense? Hire in-house when the mobile app *is* the product, not a project — when your roadmap runs for years, the app is where your competitive advantage lives, and you'll be shipping continuously long after v1. You want that knowledge inside the building. In-house wins on the things that compound: institutional memory, deep product context, and engineers who care because it's their product too. Nobody understands the weird edge case in your onboarding flow better than the person who's been maintaining it for two years. The catch is that in-house is the slowest and most expensive way to *start*. You're paying recruiting fees, salaries, and benefits for months before meaningful code ships, and a single bad senior hire can set you back a full quarter. Build in-house when you can afford to optimize for the long game over the fast start. ### When is an agency the right call? Hire an agency when you have a defined product to ship, a deadline, and little internal capacity to manage a build yourself. A good agency is a pre-assembled team — engineers, a project manager, a designer, QA — that you point at a scope and get a shipped app back, without hiring anyone yourself. The agency model's real value isn't the code — it's the *management overhead you don't carry*. You don't recruit, run sprint planning, or cover for someone on vacation. You approve scope and review demos. For a founder without a technical co-founder, that's the whole point. Agencies are the right call for MVPs (typically [$15–30K](https://nerdy.pro/blog/flutter-app-development-cost-2026)) and full business apps ($30–60K) where the goal is "get this shipped well, fast." We've done exactly this for clients like [ExtraETF](https://nerdy.pro/portfolio/extraetf) (a chart-heavy fintech app) and [YouMi](https://nerdy.pro/portfolio/youmi) — a defined product delivered by a team that had shipped that risk category before. Where agencies stop being the answer: when you need long-term ownership and the app never really "ends." At that point you're renting a team forever, and the knowledge-retention math starts favoring in-house or augmentation. ### When is staff augmentation the best fit? Flutter staff augmentation is when you embed one or more vetted Flutter engineers directly into your existing team — they work in your repo, your sprints, and your Slack, under your management, but they're employed and supplied by an external partner. You're extending a team you already have, not outsourcing a project. This is the right fit when you have an engineering function that *works* — a tech lead, a codebase, a process — and the only thing you're short on is Flutter throughput. Maybe you need to hit a launch date, cover a parental leave, or add mobile to a backend-strong team. Augmentation gives you senior Flutter capacity in days instead of the months an in-house hire takes, and the code and context stay in your repo, not a vendor's. It's also the most flexible model by a wide margin. Scale from one engineer to four for a crunch, then back down, on monthly boundaries — no severance, no recruiting cycle. That flexibility is the whole product. The honest limit: augmentation only works if you can actually manage engineers. If you don't have someone to own architecture, review PRs, and run planning, augmented engineers will drift — and you'll get agency-quality problems at augmentation prices. If that's you, hire an agency instead. This is our primary [team augmentation](https://nerdy.pro/services/team-augmentation) offering, and it's also the model we'll talk you *out* of if you don't have the management capacity to make it work. ### How fast can each model get you shipping? Speed-to-first-commit ranks in a clear order, and it's often the deciding factor. - **Staff augmentation: days to ~2 weeks.** A vetted engineer who already knows Flutter joins your existing setup. The ramp is your codebase and domain, not the language or the tooling. - **Agency: 1–3 weeks to kickoff, then continuous.** The team is pre-formed, so there's no hiring delay — the lead time is scoping and contracts, after which a whole team moves at once. - **In-house: 2–4 months before real code, sometimes more.** Sourcing, interviewing, notice periods, then ramp. For senior Flutter engineers the search runs longer — the pool is real but competitive. If your constraint is a date, that ordering matters more than the hourly rate. The cheapest engineer who starts in three months isn't cheaper than a pricier one who starts next week — not if the delay costs you a launch window. ### What are the hidden costs and failure modes of each? Every model has a failure mode that doesn't show up in the quote. Here are the ones that actually bite. **In-house hidden costs.** Recruiting is slow and expensive — 15–25% of first-year salary in fees, plus weeks of your senior engineers' time interviewing. Then there's **bus factor**: if one person owns all the Flutter knowledge and leaves, you have a crisis, not an inconvenience. And a mis-hire is the most expensive mistake of all — months of salary, lost momentum, and the search to run again. **Agency hidden costs.** The big two are **lock-in** and the **knowledge cliff**. If the agency owns the codebase, the CI/CD, and all the tribal knowledge, moving away from them is painful by design. Protect yourself contractually: code in *your* repos from day one, documented handoff, no critical infrastructure only they can access. The other failure mode is the mismatch where a $30K MVP gets quoted at $200K — shops that only sell at one price point. If the estimate doesn't match the scope, the agency either doesn't understand the scope or doesn't want to. **Staff augmentation hidden costs.** The load you can't outsource is **management**. Augmented engineers need onboarding, code review, and direction like any team member — skip it and quality slips before you notice. There's a real ramp cost while they learn your domain, plus a communication tax across a distant timezone. Augmentation is capacity, not autopilot; you're trading a hiring burden for a management one. ### How do you vet a Flutter developer or team? Whether you're interviewing an in-house candidate, an agency, or an augmentation engineer, the signal is the same. Ask for evidence, not adjectives. - **Real shipped apps.** Ask for App Store and Google Play links, not screenshots — then download them. Do they feel fast? Do they handle offline, errors, and slow networks gracefully? Shipped-and-live beats a polished portfolio deck. - **pub.dev and open-source footprint.** Published packages, meaningful contributions, or answered issues signal someone who operates in the ecosystem, not just consumes it. [Ours are public](https://nerdy.pro/open-source) if you want a reference point for what "maintained" looks like. - **State-management fluency.** They should have a *reasoned* opinion on Riverpod vs. BLoC vs. Provider — not "I use X because it's what I know." Dogma is a yellow flag; tradeoff-awareness is a green one. - **CI/CD and release discipline.** Can they set up automated builds, signing, and store deployment? Do they use feature flags and staged rollouts? Shipping is a skill separate from coding — it's where junior teams quietly bleed time. - **Testing.** Ask what they test and why. "We test everything" is as much a warning sign as "we don't test." You want judgment about where tests earn their keep. - **Platform-channel and native experience.** Real apps eventually need native code — an iOS SDK, an Android permission quirk, a native module. Someone who's never left the Dart layer will stall the first time you hit the platform boundary. Still deciding between Flutter and something else before you hire for it? We cover that in [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026) and [Flutter vs Native in 2026](https://nerdy.pro/blog/flutter-vs-native-2026). ### A decision framework: four questions Answer these in order. The first "yes" that fits routes you to a model. 1. **Is this app core to your business for the next 3+ years, and can you wait 2–4 months to staff it?** → **Hire in-house.** You want the knowledge in the building, and you can afford the slow start. 2. **Do you need a defined product shipped on a deadline, with little internal engineering to manage it?** → **Hire an agency.** Buy the team and the management overhead, not just the code. 3. **Do you already have a working engineering team that just needs more Flutter capacity — and someone to manage it?** → **Use staff augmentation.** Extend the team you have. 4. **Are you unsure whether the idea is even worth building yet?** → **Start with an agency-built MVP.** It's the fastest way to a real answer, and you're not committing to a hire before you know you need one. If two answers fit, the tiebreaker is management capacity: augmentation and in-house both assume you can run engineers. If you can't, an agency is the honest choice. ### FAQ #### How much does it cost to hire a Flutter developer? In 2026, blended agency rates run $150–250/hour in the US, $100–180 in Western Europe, $50–95 in Eastern Europe, and $25–55 in South/Southeast Asia. In-house senior salaries run roughly $120k–180k+ (US), €60k–100k (Western Europe), and $35k–70k (Eastern Europe), plus 25–40% for fully loaded cost. Staff augmentation typically costs 10–30% less than a full agency rate because you're buying the engineer, not the whole delivery team. #### Is it cheaper to hire in-house or an agency? It depends on time horizon. An agency is cheaper to start — no recruiting fees, no salaries before code ships, and no long-term liability. In-house is cheaper per hour once you've hired, so it wins over multiple years of continuous work. For a one-time build, an agency is almost always cheaper all-in; for a multi-year core product, in-house usually wins. #### What is Flutter staff augmentation? Flutter staff augmentation is when you embed one or more vetted Flutter engineers directly into your existing team. They work in your repository, your sprints, and under your management, but they're employed and supplied by an external partner. It's a way to add Flutter capacity in days without the recruiting time or long-term commitment of a full-time hire, while keeping the code and knowledge inside your own team. #### How long does it take to onboard a Flutter developer? A staff-augmentation engineer who already knows Flutter is typically productive in your codebase within days to two weeks — the ramp is your domain, not the language. An agency team kicks off in 1–3 weeks. A new in-house hire takes 2–4 months from starting the search to shipping real code, because of sourcing, interviewing, and notice periods. #### How do I know if a Flutter developer is good? Ask for live App Store and Google Play links and actually use the apps — test them on slow networks and offline. Look for a pub.dev or open-source footprint, a reasoned opinion on state management (Riverpod vs. BLoC vs. Provider), and real CI/CD and release experience. The strongest signal is platform-channel work: someone who has integrated native iOS/Android code has hit the hard parts of real apps. #### In-house vs outsource Flutter — which should a startup choose? Most early-stage startups should outsource the first build to an agency, then hire in-house once the product has traction and the roadmap is clear. Outsourcing gets you to market faster and defers the cost and risk of hiring before you know what you need. Switch to in-house when the app becomes core to the business and you'll be shipping continuously for years. #### Can I hire a dedicated Flutter team without hiring employees? Yes. A dedicated Flutter team through an agency or staff augmentation gives you engineers who work only on your product, without the employment liability, recruiting time, or benefits overhead of full-time hires. With augmentation the team works inside your processes and repo; with an agency they work as a managed unit. Both let you scale the team up or down at contract boundaries. ### Which model fits you? Not sure which of the three is right? That's the normal state — the answer depends on your timeline, your existing team, and how central this app is to the business. We're happy to talk it through honestly, even when the answer is "hire in-house" or "you need an agency, not us." If it turns out to be augmentation, that's our wedge: we place vetted Flutter engineers into your team, in your repo and your sprints, with the code and knowledge staying yours. If it's a defined product you'd rather hand off, that's our [Flutter app development](https://nerdy.pro/services/flutter-app-development) practice — see the kind of work it produces in [Arcana](https://nerdy.pro/portfolio/arcana), [ExtraETF](https://nerdy.pro/portfolio/extraetf), and [YouMi](https://nerdy.pro/portfolio/youmi). Either way, [tell us what you're building](https://nerdy.pro/contact) and we'll give you a straight recommendation on the model — not a pitch for the most expensive one. --- *Ilya Nixan is Founder & Lead Developer at [Nerdy Production](https://nerdy.pro/), a Flutter-first agency that ships apps across fintech, healthcare, and retail — and places Flutter engineers into client teams.* ## How We Build With AI: Architecture and Flexibility Over Keystrokes https://nerdy.pro/blog/building-with-ai **TL;DR.** We use AI to write a large share of our code, and that is a deliberate choice, not a shortcut. The reason is simple: an agent is fast at producing code and bad at owning a system. So we let it do the typing and we move our engineers up the stack — to architecture, to flexibility, to the whole-system decisions that decide whether a codebase is still pleasant to change a year from now. The industry caught up to this in 2026: [84% of developers now use AI tools daily](https://medium.com/@umarhussainkhokhar1234/the-developers-world-in-june-2026-everything-that-s-changing-right-now-1de29f6d695e), and the winning pattern is "generate aggressively, verify rigorously." The catch is that AI optimizes every file locally and *nobody is optimizing the codebase globally* — which is exactly why the [codebases we audit](https://nerdy.pro/blog/ai-code-audit-findings) all break the same eleven ways. Our answer is to keep a human on the part AI cannot see: the architecture. This is how we build, and why. --- ### The job changed, and we changed with it For most of software's history, the bottleneck was typing. Turning a clear idea into working code was the slow, expensive part, so that is where engineering effort went and where seniority was measured. That bottleneck is gone. As the [June 2026 state of the developer world](https://medium.com/@umarhussainkhokhar1234/the-developers-world-in-june-2026-everything-that-s-changing-right-now-1de29f6d695e) lays out, AI coding tools have moved from a curiosity to the baseline: 84% of developers reach for them every day, TypeScript overtook Python as the most-used language partly because typed code keeps AI honest, and the most productive engineers no longer write more code by hand — they *orchestrate agents* that do. The "10x engineer" became a manager of machines. We did not resist this; we reorganized around it. If an agent can produce a working feature in minutes, then the scarce, valuable human skill is no longer "can you write this function." It is "should this function exist, where does it belong, what happens when the requirements change, and how does it behave when ten thousand people hit it at once." Those are architecture and flexibility questions. They are precisely the questions AI is worst at — and the ones that decide whether a product lives or dies. The 2026 baseline, in four numbers: - **84% use AI daily.** AI coding tools are the baseline now, not an optional add-on. - **46% distrust, 3% highly trust.** Far more developers distrust AI output than trust it — which is why verification is not optional. - **Multi-agent is the new normal.** The most productive engineers direct fleets of agents instead of writing more code by hand. - **TypeScript is #1.** Typed languages overtook Python partly because they keep generated code honest. --- ### What we hand to AI, and what we keep The clearest way to describe our process is to split the work in two: the part we delegate to machines, and the part we refuse to. **We hand AI the keystrokes.** Boilerplate, CRUD endpoints, the tenth variation of a form, data-shuffling glue, first-draft tests, migrations, the tedious mechanical transforms that used to eat afternoons — agents are genuinely excellent at all of it. A model can write a correct-looking implementation faster than a person can describe it. So we let it. There is no pride in typing out a paginated list view by hand in 2026. **We keep the architecture.** Where does state live? What is the single source of truth for this domain? Which boundaries are allowed to know about which others? How does this scale from one instance to twelve? What is the failure mode when the payment provider times out? How will this shape bend when the client asks, three months from now, for the thing they swear they will never ask for? None of those have a "shortest path to running code" answer — and shortest-path-to-running-code is the only thing an agent optimizes for. This division is not a compromise. It is the highest-leverage use of both parties. The machine does what it is fast at; the humans do what only humans currently do well. The phrasing going around the industry — "Codex for keystrokes, Claude Code for commits" — is really a statement about *altitude*: the closer you get to a decision that the whole system depends on, the more a human belongs in the loop. --- ### Why architecture has to stay human Here is the uncomfortable truth we see every week, and it is the engine behind everything we just described. We run [code audits](https://nerdy.pro/services/ai-code-audit) on a steady stream of AI-built applications. The codebases are wildly different, but the [findings almost never are](https://nerdy.pro/blog/ai-code-audit-findings): secrets committed straight to the repo, no input validation, auth that never checks who is asking, no tests, the same helper duplicated four times with slight drift, two screens in one project written as if by two different teams, complex state collapsed into callback hell, code that assumes a single machine and corrupts itself the moment it runs on two. Eleven problems, over and over. Every one of those is a *whole-system* property. None of them can be produced by a single good prompt, because no single prompt can see the whole. A model generating code has a narrow context window and no memory of the decision it made three files ago. It makes the locally optimal choice every time — and **the sum of locally optimal choices is a codebase that works today and resists every change tomorrow.** The drift is not a bug in any one generation; it is the inevitable shape of generation without a global owner. So the role of the human did not shrink when AI started writing code. It *moved* — from producing lines to guaranteeing the properties that only exist across lines. Coherent architecture, a single source of truth, state modeled as state instead of a pile of racing callbacks, awareness of the real deployment environment, security that is on by default. That is the connective tissue. It is the difference between a demo and a product, and an agent left alone will skip all of it. We do not let it. --- ### Vibe and verify: how we keep AI code safe There is a real trust gap in the data, and we take it seriously: across the industry, 46% of developers actively distrust AI output and only 3% highly trust it. The mature response is not to stop using AI — it is to **treat everything it produces as untrusted third-party code until proven otherwise.** Generate aggressively; verify rigorously. "Vibe and verify." In practice that means every line an agent writes goes through the same gate a contractor's pull request would: - **It gets read by a human, not skimmed.** The confidence trap with AI code is that it *looks* finished, so the 20% that is not finished is invisible. We read for the things models reliably get wrong — authorization that is missing behind a login screen, input concatenated straight into a query, an endpoint that returns the whole user object when the UI needed a name. - **It gets tests, on purpose.** Generating a feature and generating tests for it are two different requests, and the second one rarely happens on its own. We make it happen: a smoke test that the app boots, real coverage around money and auth, and a regression test for every bug we fix. - **It gets measured against the architecture, not just "does it run."** A feature that works but invents its own state pattern, or duplicates an existing helper, or reaches across a boundary it should not know about, gets sent back — even when it passes its happy path. This is the same discipline we wrote up for founders shipping AI prototypes in [what breaks between the demo and production](https://nerdy.pro/blog/ai-prototype-to-production). The shorthand we keep coming back to: **AI gets you 80% of the way in 5% of the time — and the last 20% is the entire product.** We let AI own the 80%. We own the 20%. --- ### Orchestrating agents instead of writing more code The other shift the 2026 landscape names is multi-agent development: instead of one assistant, specialized agents running in parallel — one drafting the feature, one writing tests, one reviewing for security, one handling the mechanical migration — with an engineer directing the whole thing. This is how our engineers actually spend a day now. Less time inside a single function, more time deciding what the agents should build, in what order, against which interfaces, and then checking that what came back fits the system. It feels less like typing and more like technical leadership of a very fast, very literal team that needs precise direction and careful review. The leverage is enormous, but only if the person holding the orchestra has a score — an architecture — in their head. Point an agent fleet at a codebase with no spine and you get eleven audit findings faster. > **The leverage cuts both ways** > > Agents amplify whatever direction they are given. Pointed at a clear architecture, they make a strong codebase grow faster. Pointed at no architecture, they make a tangled one tangle faster. The human deciding the direction is not overhead on top of the AI — it is the thing that decides whether the AI is an accelerator or an avalanche. --- ### Flexibility is the whole point If there is one word for what we are optimizing for, it is not speed — speed is now cheap. It is **flexibility:** the ability to absorb a change in requirements without a rewrite. This is where the architecture-first approach pays off most visibly. The AI-built codebases we audit are fast to *start* and brutal to *change* — they calcify, not because the code is good and precious, but because no one dares touch it without tests, without a coherent structure, without knowing what else will break. That is the opposite of flexible. A product that cannot change is dead the day the market moves. When a human owns the architecture and AI owns the typing, you get both halves: the speed of generation *and* a system that bends. New requirement comes in, and there is one source of truth to update instead of four copies to hunt down; one state model to extend instead of a callback maze to untangle; one clear boundary to extend instead of a guess about which of two screens does it the "right" way. We did exactly this on [Arcana](https://nerdy.pro/portfolio/arcana), a streaming AI chat client — the unglamorous work of getting the architecture, streaming, and state model right is what turned a fast prototype into something we could keep evolving instead of rebuilding. That is the whole thesis: **we use AI so our engineers can stop spending their best hours on keystrokes and spend them on the decisions that keep software flexible.** The code is the cheap part now. The architecture is the product. --- ### Frequently asked questions #### Does using AI to write code mean lower quality? No, because we never treat AI output as finished. We follow the pattern the industry settled on in 2026: generate aggressively, then verify rigorously. Every line an agent writes is reviewed by a human like a contractor pull request, gets real tests around the parts that matter, and is checked against the system architecture before it ships. AI handles the typing; humans guarantee the quality. Used this way, the result is faster and at least as solid as fully hand-written code, because engineers spend their attention on design instead of boilerplate. #### If AI writes the code, what do your engineers actually do? They own everything AI is bad at, which is most of what determines whether software lasts: architecture, where state lives, the single source of truth for each domain, how the system scales across instances, failure modes, security posture, and flexibility for future change. They also orchestrate the agents — deciding what gets built, in what order, against which interfaces — and verify what comes back. The human work moved up the stack from producing lines to guaranteeing the whole-system properties that no single prompt can produce. #### Why not just let AI build the whole thing end to end? Because a model optimizes every file for the shortest path to code that runs, and nobody is optimizing the codebase as a whole. The result is predictable: committed secrets, no input validation, auth that never checks who is asking, no tests, duplicated logic, no consistent architecture, state collapsed into callback hell, and code that breaks the moment it runs on more than one instance. We see these same eleven problems in almost every AI-built codebase we audit. They are whole-system failures that cannot emerge one prompt at a time, so a human has to own the architecture for the AI to be safe. #### What does building with AI mean for the cost and speed of my project? It means you pay for judgment, not keystrokes. Generation is now the cheap, fast part, so the work that takes real time is the architecture, the verification, and the flexibility for future change — which is exactly the work that protects your investment. In practice you get prototypes and features dramatically faster, without inheriting the calcified, unchangeable codebase that pure AI generation tends to leave behind. You keep the speed and you keep the ability to change direction later. #### Is my AI-built codebase salvageable, or do I need a rewrite? Almost always salvageable, and almost never a rewrite. The features work and the product is real; what is missing is the connective tissue — tests, a coherent architecture, deduplicated logic, a proper state model, deployment awareness, and security. All of that can be added in place far faster and cheaper than rebuilding from scratch. That focused repair is exactly what an AI code audit delivers: we keep the foundation the AI got right and fix the whole-system parts it could not. ### Build fast, stay flexible The teams winning in 2026 are not the ones who refused AI, and they are not the ones who let it run unsupervised. They are the ones who put the machine on the keystrokes and a human on the architecture. That is how we build every project: AI for speed, engineers for the decisions that keep software flexible enough to survive its own success. If you have an AI-built app and you are not sure whether it can keep evolving, run it through an [AI code audit](https://nerdy.pro/services/ai-code-audit), or [book a free assessment](https://nerdy.pro/contact). We will tell you where the architecture is solid, where it is about to bite you, and what it takes to fix — with a fixed-scope quote, not a guess. ## AI Code Audit Findings: 11 Problems in Almost Every AI-Built Codebase https://nerdy.pro/blog/ai-code-audit-findings **TL;DR.** We run a lot of audits on apps built with Cursor, Claude Code, Bolt, Lovable, and long ChatGPT sessions. The codebases differ wildly, but the *findings* almost never do. Eleven problems show up again and again, roughly in the order they tend to hurt: **hardcoded secrets and credentials**, **no input validation** (the injection surface), **authentication that checks the box but not the request**, **zero test coverage**, **no error handling on the unhappy path**, **N+1 queries and performance left to chance**, **stale dependencies with known CVEs sitting unpatched**, **duplicated variables and functions**, **no consistent architecture**, **complex async state collapsed into callback hell instead of streams**, and **no awareness of the deployment environment**. None of these are exotic. All of them are predictable — and all of them are fixable without a rewrite. This is the engineering companion to our [founder's guide to shipping an AI prototype](https://nerdy.pro/blog/ai-prototype-to-production); if you want it handled, that is what an [AI code audit](https://nerdy.pro/services/ai-code-audit) does. --- ### What an Audit Is — and Isn't An audit is not a rewrite, and it is not a verdict on whether you should have used AI to build the thing. It is a structured read of a working codebase that answers one question: what happens the first time this meets a real attacker, a real spike in traffic, or a real change six months from now? The findings below are not hypothetical categories from a checklist — every one of them is something we have found, redacted, and fixed in a real engagement. We reference our sample audit report and the [AI code audit service](https://nerdy.pro/services/ai-code-audit) throughout, because that is where the fix actually happens; this post is the evidence for why the fix is needed. ### Why the Findings Repeat AI coding tools optimize for one thing: the shortest path to code that *runs*. That is genuinely useful — you get an idea into a working state in hours. But "runs in the demo" and "maintainable and safe in production" are different targets, and the gap between them is remarkably consistent across projects. The reason is structural. A model generating code has a narrow window of context and no memory of the decisions it made three files ago. It cannot test the thing it just wrote, it has no sense of your runway or your security posture, and it has no incentive to keep the codebase coherent over time. So it makes the locally optimal choice every time — and the sum of locally optimal choices is a codebase that works today and resists every change tomorrow. After enough audits, the failures cluster into the same eleven buckets. Here they are. --- ### 1. Hardcoded Secrets and Credentials Every audit finds this, and it is exactly the smell that costs real money: `.env` files committed to the repository, API keys and database credentials hardcoded directly in source, third-party tokens baked into client bundles that ship to every browser that loads the app. One leaked OpenAI or Stripe key can run up thousands of dollars in unauthorized charges within hours — or hand an attacker your data store outright. **Why AI does this.** Hardcoding a key works immediately; wiring up a secrets manager or environment injection does not, and the model has no reason to prefer the slower path when the faster one also "runs." The shortest path to a working feature is almost never the secure one. **How we fix it.** Secrets come out of source and into proper environment management. Anything that was ever committed to git is treated as already compromised and rotated, not just removed — a `git rm` without rotation leaves the old key valid in every clone and every commit history. This is the non-negotiable part of any audit: a leaked key is an emergency, and it gets fixed first. ### 2. No Input Validation — the Injection Surface SQL injection, XSS, and prompt injection are pervasive in AI-built code, and the root cause is the same one every time: user input goes straight into a query, a template, or an LLM prompt with no validation or sanitization in between. A single vulnerable endpoint can compromise your entire database or let an attacker manipulate what your own AI features do. **Why AI does this.** Validation is a second request the model was never asked to make. It generates the code that satisfies the happy-path prompt — "take the user's message and save it" — and the happy path never mentions what to reject. **How we fix it.** Every boundary where untrusted input enters the system gets explicit validation and parameterized queries or templates, not string concatenation. For AI features specifically, that means treating the prompt construction itself as an injection-prone boundary, not just the database layer. ### 3. Auth That Checks the Box, Not the Request AI-generated apps often implement authentication at the surface level — a login screen exists, and it works — but the backend never actually verifies permissions per request. API endpoints accept anything that hits them. Admin routes are reachable without a role check. User A can see User B's data by changing an ID in the URL — an insecure direct object reference sitting in production. **Why AI does this.** "Add a login page" and "check that this specific request is allowed to touch this specific record" are different problems, and the model solves the one it was asked about. Authentication is visible in a demo; authorization gaps are invisible until someone exploits them. **How we fix it.** We audit every endpoint against who is actually allowed to call it, not who the login screen implies is allowed, and add the per-request authorization checks that were missing — ownership checks on records, role checks on admin routes, and rate limiting on anything a script could hammer. ### 4. No Tests — Every Deployment Is a Gamble The single most common finding: **there are no tests at all.** Not a thin suite, not flaky tests — zero. The app was validated by clicking through it, and that is the entire safety net. This is invisible right up until the moment it is catastrophic. With no tests, there is no way to know whether a change broke something other than shipping it and waiting for a user to complain. Every deployment becomes a manual regression pass that nobody actually performs, so refactoring becomes terrifying, dependency updates get skipped, and the codebase calcifies — not because the code is bad, but because no one dares touch it. **Why AI does this.** Generating a feature and generating tests for that feature are two separate requests, and nobody made the second one. The model will happily write tests if asked, but left to its own devices it ships the happy path and stops. **How we fix it.** We do not aim for 100% coverage on day one. We add a thin layer where it pays off most: a smoke test that the app boots, tests around the money-handling and auth logic, and a regression test for every bug we fix during the audit. That alone turns deployments from a gamble into a routine. ### 5. No Error Handling on the Unhappy Path The app works exactly as demoed — as long as the network never drops, the third-party API never times out, and the user never does anything unexpected. The moment one of those things happens, the symptoms are ugly: an unhandled promise rejection crashes the whole request, a failed API call leaves the UI stuck on a spinner forever, an exception surfaces a raw stack trace to the user instead of a message that means anything to them. **Why AI does this.** The happy path is what the prompt described and what the demo exercised. Error handling is defensive code written for situations the model was never told to imagine, and it adds lines without making the demo look any more impressive — so it is the first thing skipped under an implicit time budget. **How we fix it.** We walk every external call — API, database, file system — and add the failure branch: retries with backoff where retrying helps, a fallback or a clear error state where it does not, and logging that tells you what actually happened instead of a generic "something went wrong." The goal is that a third-party outage degrades your app gracefully instead of taking it down. ### 6. N+1 Queries and Performance Left to Chance The list screen that loads instantly with ten rows in development grinds to a crawl with ten thousand in production. The classic cause is an N+1 query: one query to fetch a list, then a separate query per row to fetch its related data, so a screen that should cost one round trip to the database costs hundreds. Missing indexes, unbounded result sets with no pagination, and loading entire objects when only a field or two is displayed are the usual companions. Our [guide to databases and indexes](https://nerdy.pro/blog/databases-and-indexes) covers the mechanics of why this is slow and what a healthy query plan looks like. **Why AI does this.** The N+1 pattern is the most obvious way to write the loop, and it produces correct output — the model has no feedback loop that tells it the query count matters until someone measures it under real data volume, which a demo with a handful of rows never does. **How we fix it.** We profile the actual query patterns under realistic data volume, collapse the N+1 chains into joins or batched loads, add the missing indexes, and put pagination or limits on anything that returns an unbounded set. This is usually the single highest-leverage performance fix in an audit, because one bad list screen can account for most of a page's load time. ### 7. Dependency and CVE Drift `npm audit` or its equivalent turns up a wall of known vulnerabilities the moment anyone runs it — because nobody had. Packages are pinned to whatever version was current when the AI tool scaffolded the project, transitive dependencies nobody chose directly carry their own CVEs, and there is no process for finding out when a patch ships. **Why AI does this.** The model picks a package that solves the immediate problem and moves on; it has no ongoing relationship with your project that would prompt it to revisit that choice later. Dependency hygiene is a maintenance activity, and nothing about generating a feature triggers maintenance. **How we fix it.** We run a dependency vulnerability scan, patch or replace anything with a known exploit, and set up a process — even a simple scheduled scan — so this does not silently drift again the moment the audit ends. ### 8. Duplicated Variables and Functions Open an AI-built codebase and search for the same date-formatting helper. You will often find it three or four times — slightly different each time, because each was generated in isolation for the screen that needed it. The same goes for validation rules, API clients, currency math, and configuration constants. Duplication is not just ugly; it is a correctness time bomb. When the logic needs to change — a new tax rule, a fixed rounding bug, an updated endpoint — you have to find every copy. You will miss one. Now two parts of the app disagree about something they should agree on, and that disagreement is the next production incident. **Why AI does this.** The model rarely searches the existing codebase for a helper it could reuse. It is cheaper, from its perspective, to regenerate the function inline than to discover and import the one that already exists. Each generation is locally reasonable; the aggregate is drift. **How we fix it.** We find the clusters of near-identical code, extract a single source of truth, and route every call site through it. This is one of the highest-leverage cleanups in most audits: it shrinks the codebase and removes whole categories of "fixed here but not there" bugs. ### 9. No Consistent Architecture This one is jarring to see for the first time. Two screens in the *same project* will be written as if by two different teams: one fetches data in the component, the other through a service layer; one holds state one way, the next does it completely differently; naming, folder structure, and error handling change from feature to feature. There is no spine. A codebase with no consistent architecture is one where every file you open is a surprise. Onboarding a developer takes weeks because there is no pattern to learn — only a hundred special cases to memorize. Worse, when patterns conflict, the seams between them are exactly where bugs breed. **Why AI does this.** The model has no persistent picture of "how this app is built." Each prompt is a fresh start, so it reaches for whatever pattern fits that one request. Over a project's life that produces a patchwork — every piece sensible alone, the whole thing incoherent. **How we fix it.** We pick one architecture that fits the project — not a dogmatic one, a *fitting* one — and converge the codebase onto it incrementally, so a developer who learns one feature can predict how the next one works. ### 10. Stream-Based State Avoided — Straight Into Callback Hell This is the most technically interesting failure, and the one that quietly breaks the hardest features. AI-generated code tends to **avoid stream- and reactive-state models** in favor of imperative callbacks. Instead of modeling "this value changes over time and the UI reacts," it wires up a callback, which triggers another callback, which sets a flag, which fires a third — and the result is callback hell. For simple screens you barely notice. But the moment the state is genuinely complex — a multi-step form with cross-field validation, a live-updating dashboard, anything with debouncing, retries, cancellation, or optimistic updates — the callback approach falls apart. The classic symptom is the form that *almost* works: it validates, but the error clears at the wrong moment; it submits, but a double-tap fires it twice. **Why AI does this.** Imperative callbacks are the most common pattern in its training data and the easiest to generate one piece at a time. Reactive and stream-based models require holding the whole state machine in mind at once — exactly what a context-limited generator is worst at. **How we fix it.** We identify the complex-state features and rebuild their state layer properly — as streams or a reactive state model appropriate to the stack — so the UI is a function of state rather than a pile of callbacks racing each other. ### 11. No Awareness of the Deployment Environment The model writes code as if it will run as a single process on one machine — because from inside the prompt, that is the only environment it can see. It has no idea how many instances will run, what managed services already exist, or how traffic is routed. So it defaults to the simplest possible topology, and that default quietly breaks the moment the app is deployed for real. The symptoms are always the same. State that lives in process memory — a cache, sessions, rate-limit counters — works perfectly on one instance and silently diverges the moment a second replica comes up behind the load balancer. Background jobs fire on *every* instance instead of once, so the email goes out three times. **Why AI does this.** It has no picture of your infrastructure. It does not know you already have Redis, a message queue, and object storage — so it reimplements them in memory. The deployment topology is exactly the context a prompt cannot contain. **How we fix it.** We map the actual deployment and move shared state to where it belongs: cache, sessions, and locks into Redis or the database, files into object storage, recurring work onto a real scheduler or queue. The result is code that scales horizontally. --- ### How We Find Them Every finding above starts with an automated pass and ends with a human reading the code. The automation — static analysis, dependency and secrets scanning, API cost profiling — is what makes an AI-accelerated audit fast: it clears the categories that are mechanical to detect (findings 1, 4, 6, and 7 above surface here almost immediately) so the time our engineers spend is concentrated on the categories that require judgment — authorization logic, architecture, and whether a given error path actually matters for your product. Neither half works alone: automation alone misses everything that requires understanding what the code is *for*, and manual review alone does not scale to a real codebase in a week. The full breakdown of the process is on the [AI code audit service page](https://nerdy.pro/services/ai-code-audit). ### What You Get in the Report Findings do not arrive as a raw list. Every one is ranked by severity — critical, high, medium, low — with a plain-language explanation of the risk, proof of concept where it applies, and a specific fix recommendation, the same shape every finding above followed. If you want to see the format before committing to anything, [request a redacted sample audit report](https://nerdy.pro/contact?intent=ai-code-audit) — the same report structure a real engagement produces, with client-identifying details removed. ### The Pattern Behind the Pattern Step back and the eleven findings share one root cause: **AI optimizes each generation locally, and nobody is optimizing the codebase globally.** Secrets management, input validation, authorization, tests, error handling, query performance, dependency hygiene, deduplication, architecture, state modeling, and deployment awareness are all *whole-system* properties. They cannot emerge one prompt at a time, because no single prompt can see the whole. That is precisely the gap a human review closes. The reassuring part is that none of this means the AI-built foundation is wasted. The features work; the product is real. What is missing is the connective tissue — and adding it is far faster than rebuilding from scratch. --- ### Frequently Asked Questions #### What does an AI code audit actually find? Across dozens of AI-generated codebases, eleven problems recur almost every time: hardcoded secrets and credentials, no input validation, authentication that checks the box but not the request, zero automated tests, no error handling on the unhappy path, N plus one queries and performance left to chance, stale dependencies with known CVEs, heavily duplicated variables and functions, no consistent architecture across the project, complex async state implemented as callback hell instead of streams, and code written with no awareness of the deployment environment. The specific code differs from project to project, but these eleven categories show up again and again. #### Why does AI-generated code leave secrets and API keys exposed? Hardcoding a key works immediately, while wiring up a secrets manager or environment injection does not, and the model has no reason to prefer the slower, correct path when the faster one also runs. The result is .env files committed to the repository, keys hardcoded in source, and tokens baked into client bundles. Anything that was ever committed to git has to be treated as already compromised and rotated, not just deleted, because the old value stays valid in every clone and every commit history. #### Why does AI-generated code not validate user input? Validation is a second request the model was never explicitly asked to make. It generates code that satisfies the happy-path prompt, and the happy path never mentions what to reject, so user input often goes straight into a query, a template, or an LLM prompt with no sanitization in between. That is the direct cause of the SQL injection, XSS, and prompt injection vulnerabilities that show up in nearly every AI-built codebase we review. #### Why does AI-generated code have no tests? Because writing a feature and writing tests for it are two separate requests, and the second one usually never happens. AI tools optimize for the shortest path to code that runs, which is the happy path with no test suite behind it. The model will write tests when asked, but on its own it ships the feature and stops, leaving every future deployment without a safety net. #### Why is there so much duplicated code in AI-built apps? The model rarely searches the existing codebase for a helper it could reuse. Regenerating a function inline for the screen that needs it is cheaper, from its perspective, than discovering and importing one that already exists. Each generation is locally reasonable, but the result is the same logic copy-pasted several times with small differences, which becomes a correctness problem the moment that logic needs to change. #### Why do complex forms built by AI often not work properly? AI-generated code tends to avoid stream-based and reactive state models in favor of imperative callbacks. For simple screens that is fine, but complex state such as multi-step forms with cross-field validation, debouncing, retries, or optimistic updates collapses into callback hell. The classic symptom is a form that almost works: errors clear at the wrong moment, or a double-tap submits twice. Modeling the state as a stream fixes it. #### Why does AI-generated code fall over under real traffic? The most common cause is an N plus one query pattern: one query to fetch a list, then a separate query per row for its related data, so a screen that should cost one database round trip costs hundreds. It looks correct in development with a handful of rows and only shows up once real data volume hits it, because nothing in the generation process measures query count. Missing indexes and unbounded result sets with no pagination are the usual companions. #### Why does AI-generated code break when it runs on more than one instance? Because the model has no picture of how the app is deployed. It writes code for a single process on one machine, so it keeps state in memory and writes files to local disk. That works on one instance but breaks the moment the app is parallelised behind a load balancer: in-memory caches, sessions, and rate-limit counters diverge across replicas, and background jobs fire on every instance instead of once. The fix is to move shared state to the right backing services so the app scales horizontally. #### Do I need to rewrite my AI-built app to fix these problems? No. All eleven findings are fixable in place. Secrets are removed and rotated, input gets validated at every boundary, authorization checks go on every endpoint that needs them, tests are added where they pay off most, failure paths get proper error handling, N plus one queries get collapsed into batched loads, dependencies get patched, duplicated logic is extracted into a single source of truth, the codebase converges onto one consistent architecture, complex-state features get a proper reactive state layer, and shared state moves to the right backing services. This keeps the working foundation the AI produced and only fixes the connective tissue that is missing, which is far faster and cheaper than a rebuild. ### Get the Findings for Your Codebase If you have an AI-built app and you recognize any of these eleven, you are not behind — you are exactly where almost every AI-generated codebase lands. The fix is not a rewrite; it is a focused audit that adds the connective tissue the AI could not. Run your codebase through an [AI code audit](https://nerdy.pro/services/ai-code-audit), [request a sample audit report](https://nerdy.pro/contact?intent=ai-code-audit) to see the format first, or [book a free assessment](https://nerdy.pro/contact) and we will tell you which of the eleven is your biggest risk, what it takes to fix, and give you a fixed-scope quote — not a guess. ## Shipping an AI-Built Prototype to Production: What Breaks and How to Fix It https://nerdy.pro/blog/ai-prototype-to-production **TL;DR.** An AI coding tool can get you a working prototype in a weekend, but "works in the demo" and "safe to run in production" are two different things. The same three problems show up in almost every AI-built app: **security holes** (exposed keys, missing authorization, injection), **runaway API costs** (5–20× overspend from uncached, unbatched, unbounded calls), and **data leaks** (sensitive info in logs, over-sharing API responses, third-party exposure). You do not throw the prototype away — you audit it, fix those three categories, and ship. This post is the playbook: what breaks, why, and the step-by-step path from prototype to production. If you'd rather hand it off, that's exactly what an [AI-generated code audit](https://nerdy.pro/services/ai-code-audit) does. If you just want the process, jump to [how to ship it](#how-to-take-an-ai-built-prototype-to-production). --- ### The prototype that works in the demo and breaks in production You built something real. Cursor, Bolt, Lovable, or a long ChatGPT session turned an idea into a running app in days instead of months. It looks great. It works on your machine. You demo it to early users or investors and the feedback is good. So you point it at a domain, flip it to production, and start sending real people to it. That's where the trouble starts — and it's not because you did anything wrong. AI agents are extraordinarily good at producing code that *works*. They are not good at producing code that's *production-ready*, because the two goals are different. "Works" means the happy path runs. "Production-ready" means it survives an attacker, a traffic spike, a bad input, and a billing cycle — none of which appear in a demo. This isn't a fringe concern. [Veracode tested 100+ LLMs](https://www.veracode.com/blog/genai-code-security-report/) and found 45% of generated code fails security tests. [Apiiro's 2025 analysis](https://apiiro.com/blog/4x-velocity-10x-vulnerabilities-ai-coding-assistants-are-shipping-more-risks/) found AI-assisted developers expose credentials nearly twice as often. [Stanford researchers](https://ee.stanford.edu/dan-boneh-and-team-find-relying-ai-more-likely-make-your-code-buggier) found developers using AI assistants write *less* secure code while being *more* confident it's safe. The confidence gap is the dangerous part: the demo feels finished, so the 20% that isn't finished is invisible until it costs you. The good news is that the gap is predictable. After auditing dozens of AI-generated codebases, the failures cluster into three buckets — the same three the prototype never had a reason to handle. --- ### The three things that break #### 1. Security holes AI tools optimize for the shortest path to a working feature, and the shortest path is almost never the secure one. - **Exposed keys and secrets.** API keys, database credentials, and service tokens get hardcoded into source, baked into client bundles, or committed in `.env` files. One leaked third-party key can rack up thousands of dollars in unauthorized usage within hours — or hand an attacker your data store. - **Authentication without authorization.** There's a login screen, so it *feels* secure. But the backend often doesn't check who's actually allowed to do what. Endpoints accept any request; change an ID in the URL and you're reading someone else's records. This — broken object-level authorization — is the single most common flaw we find, and it's at the top of the [OWASP API Security list](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/) for a reason. - **Injection.** SQL injection, XSS, and prompt injection are everywhere in AI-built code, because models happily concatenate user input straight into queries, HTML, or LLM prompts. One unsanitized endpoint can expose the whole database or let an attacker steer your AI. - **Insecure storage.** Personal data in plain text, session tokens in `localStorage`, passwords hashed with MD5 or not at all. AI defaults to the simplest implementation, which is rarely the safe one. (We wrote a full walkthrough of [how data should actually be encrypted at rest and in transit](https://nerdy.pro/blog/encryption-explained) — it's the reference we point clients to.) #### 2. Runaway API costs This is the one that quietly bankrupts a launch instead of breaching it. - **Redundant calls.** AI-generated apps call the same paid endpoint — geocoding, exchange rates, an LLM, a verification API — over and over for data they already have. No caching, no memoization. We routinely find apps where 60–80% of API spend is pure waste. - **No budgets or rate limits.** No per-user cap, no daily ceiling, no circuit breaker. A single enthusiastic user, a scraper, or a bot can burn your entire monthly budget in an afternoon. The AI has no concept of your pricing tiers or your runway, so it never adds the guardrails. - **No batching.** Firing individual requests inside a loop instead of using the batch endpoint the provider offers. At demo scale it's invisible; at production scale it's a five-figure monthly bill. The [cost of building an app](https://nerdy.pro/blog/flutter-app-development-cost-2026) is one thing — the cost of *running* an unoptimized one is the surprise that catches founders off guard. #### 3. Data leaks The category that turns a quiet success into a compliance incident. - **Logging sensitive data.** AI loves verbose logging. Emails, passwords, payment details, and personal data end up in application logs and error trackers, often retained indefinitely and readable by anyone with dashboard access. - **Over-sharing through APIs.** Endpoints that return the entire user object — hashed passwords, internal IDs, metadata — when the frontend only needed a display name. GraphQL endpoints with no depth limit that let an attacker walk your whole data model. - **Third-party exposure.** Analytics, monitoring, and error tools wired in without a thought for what flows to them. User behavior and personal data leave your system without consent, which is exactly the kind of thing GDPR fines are written for. --- ### How to take an AI-built prototype to production You don't rebuild — that throws away the 80% the AI got right. You audit the prototype, fix the three categories above in priority order, and ship. Here's the path we run. #### Step 1 — Freeze and scope The first instinct after a good demo is to add more features. Resist it. Adding code to an unaudited base just multiplies the surface area you'll have to fix. Lock the scope, grant read access, and decide what the review covers — a full pass, or a focused one on whichever pillar scares you most. Nothing in production changes yet. #### Step 2 — Automated scan Static analysis, dependency and secrets scanning, and cost profiling catch the obvious problems fast: the committed API key, the package with a known CVE, the endpoint hit 300 times a minute. This is the cheap, high-volume layer. #### Step 3 — Manual expert review The expensive problems hide where tools can't see them: an endpoint that returns the right data but never checks *who's asking*, a billing loop that's correct but uncached, a third-party integration quietly shipping PII offsite. A human reads the architecture, the auth flows, and the data paths. This is the step that separates an [AI-generated code audit](https://nerdy.pro/services/ai-code-audit) from a linter. #### Step 4 — Prioritized fixes Findings get sorted by severity and fixed in order. Critical security holes first — a leaked key or an auth bypass is an emergency. Then cost controls, because every day at 5–20× overspend is real money. Then privacy and compliance. You review and approve every change before it merges; nothing happens to your code without your sign-off. #### Step 5 — Ship with confidence Re-test against every finding, confirm the patches hold, and deploy. Same product, same AI-built foundation — now hardened. That's the whole point: you keep the speed the AI gave you and add the durability it couldn't. > We've run exactly this on real builds. [Arcana](https://nerdy.pro/portfolio/arcana), a streaming AI chat client, went from prototype to a shipped, production-grade app — the unglamorous work of hardening streaming, auth, and data handling is what turned a demo into something people could actually rely on. --- ### What it costs and how long it takes A focused security pass on a small single-service app runs **1–2 weeks**; a full production-readiness audit — security, cost, data, performance, infrastructure — on a typical multi-service app is **2–4 weeks**. That's an order of magnitude faster and cheaper than the 3–6 months a from-scratch rebuild would take, because you're keeping everything the AI got right and only fixing what it got wrong. The mental model worth internalizing: **AI gets you 80% of the way in 5% of the time.** The last 20% — security, cost control, data privacy, error handling — is the entire difference between a demo and a product. An audit buys that 20% without throwing away the foundation. Full tiers and pricing are on the [AI-generated code audit](https://nerdy.pro/services/ai-code-audit#ai-code-audit-pricing) page. --- ### Frequently asked questions #### Can I take a Cursor, Bolt, or Lovable prototype to production as-is? Rarely without changes. The prototype almost certainly works on the happy path, but AI tools systematically skip the parts that only matter in production: authorization checks, API cost controls, secure data storage, and privacy handling. The fix is not a rebuild — it is an audit that finds those gaps and patches them, usually in one to four weeks, keeping the code the AI already got right. #### What is an AI code audit? An AI code audit is a structured review of an AI-generated codebase that finds and fixes the issues AI coding tools systematically miss: security vulnerabilities, runaway API costs, and data leaks. It combines automated scanning (secrets, dependencies, static analysis) with manual expert review of architecture, authentication, and data flows, then delivers prioritized fixes — or implements them for you. #### Why does AI-generated code have so many security problems? AI models optimize for the shortest path to working code, and that path is almost never the secure one. They hardcode secrets, build authentication without real authorization, concatenate user input into queries and prompts, and store data in plain text. Independent studies from Veracode, Apiiro, and Stanford all found that AI-assisted code is measurably less secure — while developers feel more confident it is safe. #### How much can an AI prototype overspend on API costs? Typically 5 to 20 times more than necessary. The usual causes are calling the same paid endpoint repeatedly without caching, making individual requests in a loop instead of batching, and having no per-user rate limits or daily budget caps. We have seen monthly API bills drop from thousands of dollars to a few hundred after these controls are added. #### Do I have to rebuild my app to make it production-ready? No, and you should not. A rebuild throws away the 80% the AI got right and costs three to six months. An audit keeps that foundation and fixes only the 20% that breaks in production — security, cost, privacy, and error handling — in one to four weeks. You ship the same product, hardened. #### Will auditing the app take it offline? No. The audit reviews source code and runs read-only tests against a staging environment, so your live app keeps running normally. If a critical vulnerability that poses immediate risk is found, you are notified right away so you can decide how to respond. ### Ship the thing you already built You did the hard part — you found something worth building and got it running. Don't let the invisible 20% turn a real success into a breach, a surprise bill, or a compliance problem. Run the prototype through an [AI-generated code audit](https://nerdy.pro/services/ai-code-audit), fix the three things that break, and put it in front of real users with confidence. If you've got an AI-built app you're nervous about deploying, [book a free assessment](https://nerdy.pro/contact). We'll tell you which of the three pillars is your biggest risk, what it'll take to fix, and give you a fixed-scope quote — not a guess. ## Flutter vs Native (iOS/Android) in 2026: When Cross-Platform Wins and When It Doesn't https://nerdy.pro/blog/flutter-vs-native-2026 We run a Flutter agency, so you'd expect us to say "Flutter, always." We don't. We've shipped both, and we've told clients to go native when native was the right call. This is the honest version of the comparison — what actually differs in 2026, with real numbers, and a decision rule you can apply in five minutes. If you're weighing Flutter against React Native instead, that's a different question — we cover it in [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026). This post is about Flutter vs **native** (Swift/SwiftUI on iOS, Kotlin/Jetpack Compose on Android). ### The short answer For the large majority of apps in 2026, **Flutter is the better business decision**: one codebase for iOS, Android, and web, roughly **30–40% lower build cost** than two native apps, and a single team to maintain it. Native wins in a narrower set of cases — apps where the platform *is* the product: heavy AR/VR, complex on-device ML, deep OS integration, games, or apps where day-one access to brand-new OS features is a competitive requirement. The decision rule: **if your differentiator is the product and the speed of shipping it, choose Flutter. If your differentiator is squeezing the hardware or the OS itself, choose native.** ### Flutter vs native at a glance | Dimension | Flutter | Native (iOS + Android) | | --- | --- | --- | | Codebases to build & maintain | 1 | 2 | | Typical build cost | Baseline | ~1.5–1.8× higher | | Time to ship both platforms | Fastest | Slowest (two teams, two timelines) | | Runtime performance | Excellent for ~95% of apps | Best possible | | UX/UI fidelity | Near-native, fully custom | Platform-perfect by default | | Access to newest OS features | Hours–weeks behind (via plugins) | Day one | | Team & hiring | One Flutter team | iOS team + Android team | | Best for | Most product apps, MVPs, e-commerce, fintech, content, internal tools | Games, AR/VR, heavy on-device ML, OS-level utilities | ### What's the actual cost difference? Building two native apps means two codebases, often two teams (iOS and Android specialists rarely overlap), and two sets of bugs to fix for every feature. Flutter collapses that into one. In our projects, Flutter comes in **30–40% cheaper** than the equivalent two native apps — and the gap *widens* over the app's lifetime, because every future feature and fix is written once instead of twice. We broke down where that 40% actually comes from (and where founders lose it) in [What a 40% Mobile Cost Reduction Actually Looks Like](https://nerdy.pro/blog/mobile-cost-reduction-case-studies). For ballpark build costs by tier, see [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026). The cost advantage is smallest for a single-platform app (if you only ever need iOS, native Swift is a fair fight) and largest for anything that has to run on iOS, Android, and web at once. ### Is Flutter's performance good enough? For roughly 95% of apps, yes — and users can't tell the difference. Flutter compiles to native ARM code and renders its own UI with its own engine (the same Skia/:term{slug="impeller"} lineage that powers Chrome and Android), so it isn't a webview and it isn't a bridge bottleneck like older cross-platform tools. Where native still has a measurable edge: sustained 60–120fps under heavy graphics load, real-time camera/video processing, large on-device ML inference, and AR/VR. If your app's core loop is one of those, native's direct access to Metal, Core ML, or the camera stack is worth the extra cost. For a CRUD app, a marketplace, a banking app, a delivery app, or a content app, that edge is invisible. ### When should you choose native instead? Be honest with yourself here — these are the cases where we'd tell you to go native: - **Games and graphics-heavy apps.** Use a game engine or native, not Flutter. - **AR/VR experiences** that lean on ARKit/ARCore deeply. - **Heavy on-device ML** (large models, real-time inference) where Core ML / NNAPI access matters. - **OS-level utilities** — widgets-as-the-product, deep Apple Watch / Wear OS, CarPlay/Android Auto, system extensions. - **You must ship a brand-new OS feature on day one** of Apple's or Google's release, as a competitive requirement. - **Single platform forever** — if you'll genuinely only ever have iOS, native Swift removes the abstraction layer with no cross-platform cost to offset it. ### When is Flutter the clear winner? - **You need iOS and Android (and maybe web) from one budget and one team.** - **You're building an MVP** and want to validate fast without funding two parallel builds. - **E-commerce, fintech, food delivery, content, booking, internal/B2B tools** — the categories where most apps live. - **You're migrating from a Telegram bot or a web app** and want a real app quickly (see [Telegram Bot to App in 6 Weeks](https://nerdy.pro/blog/telegram-bot-to-app-6-weeks)). - **You want design consistency** across platforms and full control over a custom-branded UI. ### Does a Flutter app look and feel native? Yes — when it's built well. Flutter can render Material (Android) and Cupertino (iOS) widgets so the app respects each platform's conventions, and because it draws its own pixels you also get complete freedom for custom-branded UI. The failure mode isn't Flutter; it's a team that ships one generic look on both platforms and ignores platform gestures, navigation patterns, and system fonts. That's a craft problem, not a framework limitation. ### What about long-term maintenance and platform risk? Maintenance is where one codebase pays off most: one place to fix bugs, one dependency tree, one CI pipeline, one set of OS-update migrations instead of two. That compounding saving is usually larger than the upfront build difference. The fair counterargument is **dependency risk** — Flutter relies on Google, and on community plugins for platform features. Google's track record is mixed (Firebase Dynamic Links was sunset in 2025, which we wrote about in [Deferred Deep Linking](https://nerdy.pro/blog/deferred-deep-linking-app-clips-install-referrer)). But Flutter itself is now a mature, widely adopted, open-source framework with a deep package ecosystem, and the plugin gap for mainstream features has effectively closed. The risk is real but small for typical apps. ### Hiring and team implications Native means staffing — or contracting — two distinct skill sets: Swift/SwiftUI engineers and Kotlin/Compose engineers. They don't substitute for each other, and a feature isn't done until both teams ship it. Flutter needs one team writing Dart, which is faster to coordinate and cheaper to scale. The tradeoff: the senior Flutter talent pool is smaller than the iOS or Android pools individually, so picking a team that genuinely knows the framework matters more. ### A decision framework: answer four questions 1. **Is your core feature about squeezing the hardware or OS** (graphics, AR, on-device ML, system integration)? If yes → lean native. If no → Flutter. 2. **Do you need both iOS and Android?** If yes → Flutter's advantage is large. If you'll truly only ever have one → native is a fair fight. 3. **How fast do you need to be in market?** Faster → Flutter. Time-insensitive and performance-critical → native is affordable. 4. **What's your maintenance budget over 2–3 years?** Tighter → Flutter's single codebase wins on lifetime cost. If three or four answers point to Flutter, the decision is made. ### The honest bottom line Native produces the best *possible* app. Flutter produces the best app *for most businesses* — because "best possible" rarely justifies paying ~1.5–1.8× and running two teams when users can't tell the difference. Choose native when the platform is the product. Choose Flutter for almost everything else. ### FAQ #### Is Flutter better than native in 2026? For most business and consumer apps, yes — Flutter delivers near-native performance and UX at roughly 30–40% lower cost with one codebase. Native is better only for graphics-, AR/VR-, or hardware-intensive apps and OS-level utilities. #### Is Flutter as fast as native? For ~95% of apps, the performance difference is imperceptible — Flutter compiles to native code and renders with its own engine. Native retains an edge in sustained heavy graphics, real-time video/camera, and large on-device ML. #### How much cheaper is Flutter than building two native apps? Typically 30–40% lower to build, and the gap grows over time because every future feature and fix is written once instead of twice. #### Do big companies use Flutter? Yes — Flutter is used in production by large consumer and fintech apps worldwide and is a mature, Google-backed open-source framework with a deep plugin ecosystem. #### Does a Flutter app look native on iOS and Android? It can render platform-appropriate UI (Cupertino on iOS, Material on Android) and supports fully custom branding. Looking "non-native" is a sign of a rushed build, not a framework limit. #### When should I choose native over Flutter? Choose native for games, AR/VR, heavy on-device ML, OS-level utilities, day-one access to brand-new OS features, or if you'll only ever ship one platform. --- *Nerdy Production is a Flutter agency building apps in fintech, healthcare, and retail. If you're deciding between Flutter and native for a specific project, [tell us about it](https://nerdy.pro/contact) — we'll give you an honest recommendation, even when that's "go native." See also our [Flutter app development](https://nerdy.pro/services/flutter-app-development) service.* ## Deferred Deep Linking After Firebase Dynamic Links: App Clips + Play Install Referrer https://nerdy.pro/blog/deferred-deep-linking-app-clips-install-referrer A user taps your link — a shared product, an invite, a password reset — and the app is not installed yet. They land in the App Store or Google Play, install the app, open it, and… they are dropped on a generic home screen. The context of what they tapped is gone. That broken moment is exactly what **deferred deep linking** is meant to fix, and for years most teams solved it with [Firebase Dynamic Links](https://firebase.google.com/docs/dynamic-links). That option no longer exists. This article explains how we built a deferred deep linking system that survives an install — and can optionally wait for a business event like login before it routes the user — using **iOS App Clips** and the **Google Play Install Referrer API** instead of a third-party service. ### Key takeaways - **Deferred deep linking** preserves a link's destination across an app install, so a first launch lands on the right screen instead of the home screen. - **Firebase Dynamic Links was deprecated and shut down on 25 August 2025** — the most common solution is now gone, and existing links no longer resolve. - The popular do-it-yourself fallbacks — **device fingerprinting** and **clipboard matching** — are unreliable, privacy-fragile, and discouraged by Apple. - On **iOS**, a lightweight **App Clip** captures the link before the full app exists and hands it off through a shared **App Group** container after install. - On **Android**, the official **[Play Install Referrer API](https://developer.android.com/google/play/installreferrer)** delivers the link payload deterministically on first launch. - A **pending-link queue** lets you postpone navigation until both the install completes *and* an optional business event (such as a successful login) has fired. ### What is deferred deep linking? A **deep link** opens a specific screen inside an app — for example `myapp://product/42`. A deep link only works if the app is already installed. **Deferred deep linking** removes that precondition: when the app is *not* installed, the destination is remembered through the store visit and the install, and the app navigates to it on first launch. The hard part is the gap. Between the tap and the first launch, the operating system installs a fresh app that has no memory of what the user tapped. Carrying a payload across that gap — reliably and without invading privacy — is the entire problem. ### The standard solution is gone: Firebase Dynamic Links For most of the last decade, the default answer was [Firebase Dynamic Links](https://firebase.google.com/docs/dynamic-links). It handled the store redirect, the deferred payload, and the first-launch resolution for both platforms behind one SDK. Google [deprecated Dynamic Links](https://firebase.google.com/support/dynamic-links-faq) and **fully shut the service down on 25 August 2025**. Links stopped resolving, and there is no drop-in replacement from Google. Paid attribution platforms such as [Branch](https://www.branch.io) and AppsFlyer fill the gap commercially, but many teams want to own the flow without a per-event vendor bill or sending user clicks to a third party. > **Firebase Dynamic Links is no longer an option** > > If your deferred deep linking still depends on Firebase Dynamic Links, it stopped working on 25 August 2025. Migrating to [Universal Links](https://developer.apple.com/ios/universal-links/) and [Android App Links](https://developer.android.com/training/app-links) handles the *installed-app* case — but **not** the deferred (not-yet-installed) case. That gap is what this approach closes. ### The common DIY fallbacks — and their pitfalls Once Dynamic Links is off the table, most guides reach for one of two homegrown techniques. Both are worth understanding precisely because they are fragile. **Device fingerprinting.** The web page records a "fingerprint" (IP address, screen size, OS version, locale) at tap time. On first launch, the app sends its own fingerprint to a server that tries to match the two within a short window. The pitfalls are serious: shared/carrier IPs cause mismatches, the match window is short, accuracy drops on busy networks, and probabilistic matching of users is exactly the pattern Apple's privacy rules push against. **Clipboard / pasteboard.** The web page copies the payload to the clipboard; the app reads it on launch. Since iOS 14 this triggers a visible "pasted from Safari" banner, users can wipe the clipboard, and any other copy in between destroys the payload. | Approach | Platform | Reliability | Privacy | Status | | --- | --- | --- | --- | --- | | Firebase Dynamic Links | iOS + Android | High | Acceptable | **Shut down Aug 2025** | | Device fingerprinting | iOS + Android | Low–medium | Poor | Discouraged | | Clipboard matching | iOS | Medium | Paste banner shown | Fragile | | **App Clip + App Group (ours)** | iOS | High | Good | Recommended | | **Play Install Referrer API** | Android | High | Good | Official | ### Our approach: App Clips on iOS, Install Referrer on Android Instead of guessing who the user is after install, we capture the link *deterministically* on each platform using a first-party mechanism, then converge on a single resolution step inside the app. #### iOS: capture the link with an App Clip An [App Clip](https://developer.apple.com/documentation/app_clips) is a tiny slice of your app (under 15 MB) that launches almost instantly from a link, an App Clip Code, or a QR code — **without a full install**. That property is exactly what we need: the App Clip runs *before* the full app exists, so it can see the original link. The flow: 1. The link opens the App Clip. iOS delivers the invocation URL through an `NSUserActivity`. 2. The App Clip writes that URL (plus a timestamp) into a **shared [App Group](https://developer.apple.com/documentation/xcode/configuring-app-groups) container** that the full app can also read. 3. The App Clip shows an App Store overlay prompting the user to install the full app. 4. After install, the full app reads the pending link from the same App Group container on first launch. ```swift // App Clip — capture the invocation URL into the shared App Group func scene(_ scene: UIScene, continue activity: NSUserActivity) { guard activity.activityType == NSUserActivityTypeBrowsingWeb, let url = activity.webpageURL else { return } let shared = UserDefaults(suiteName: "group.pro.nerdy.deeplink") shared?.set(url.absoluteString, forKey: "pendingDeepLink") shared?.set(Date(), forKey: "pendingDeepLinkAt") } ``` ```swift // Full app — read it once, on first launch after install let shared = UserDefaults(suiteName: "group.pro.nerdy.deeplink") if let link = shared?.string(forKey: "pendingDeepLink") { DeepLinkQueue.shared.enqueue(link) shared?.removeObject(forKey: "pendingDeepLink") } ``` Because the App Clip and the full app share the same App Group, the payload is handed off exactly — no fingerprint matching, no clipboard prompt, no probabilistic guessing. #### Android: the Google Play Install Referrer API Android has a clean, official answer: the [Play Install Referrer API](https://developer.android.com/google/play/installreferrer/library). Attach your payload to the Play Store URL as the `referrer` parameter, and Google delivers that exact string to the app on first launch. ```text https://play.google.com/store/apps/details?id=pro.nerdy.app&referrer=deeplink%3D%2Fproduct%2F42 ``` ```kotlin val client = InstallReferrerClient.newBuilder(context).build() client.startConnection(object : InstallReferrerStateListener { override fun onInstallReferrerSetupFinished(responseCode: Int) { if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { val referrer = client.installReferrer.installReferrer // "deeplink=/product/42" DeepLinkQueue.enqueue(referrer) client.endConnection() } } override fun onInstallReferrerServiceDisconnected() {} }) ``` This is deterministic — the referrer string survives the store visit and the install untouched, with no server-side matching required. #### Converging in one place — and waiting for login Both platforms now feed the same in-app **pending-link queue**. We built exactly this for our [cashback & loyalty platform](https://nerdy.pro/portfolio/cashback-loyalty-platform), and we do it in our [Flutter app development](https://nerdy.pro/services/flutter-app-development) work with a small platform channel that surfaces the native payload to Dart, then a single resolver decides *when* to act. The "when" matters. A deep link to an authenticated screen (`/orders/42`) will fail on a cold start if the user is not logged in yet. So the resolver does not navigate immediately — it holds the destination until the app is ready *and* any required business event has fired. ```dart class DeepLinkQueue { Uri? _pending; bool _authReady = false; void enqueue(Uri link) { _pending = link; _tryResolve(); } // Called by the business layer, e.g. after a successful login void onEvent(AppEvent event) { if (event == AppEvent.loggedIn) _authReady = true; _tryResolve(); } void _tryResolve() { final link = _pending; if (link == null || !_authReady) return; // wait for both _pending = null; router.go(link.path); // safe: app is installed AND the user is logged in } } ``` The result is a deferred deep link that is robust against the two failure modes that break most implementations: the app not being installed, and the target screen not being ready. ### When this approach is the right fit This pattern shines when you control both the link source and the app, want first-party reliability, and care about privacy or vendor cost. If you only need the *installed-app* case, plain [Universal Links](https://developer.apple.com/ios/universal-links/) and [App Links](https://developer.android.com/training/app-links) are enough. If you need cross-channel marketing attribution with dashboards, a paid platform like Branch may be worth the spend. But for product flows — invites, shared content, onboarding, password resets — App Clips plus the Install Referrer API give you a deterministic deferred deep link you fully own. If you are migrating off Firebase Dynamic Links or building this from scratch, [we can help you ship it](https://nerdy.pro/contact). ### Frequently asked questions #### What is deferred deep linking? Deferred deep linking preserves a link target across an app install. When a user taps a link without the app installed, the destination is remembered, the user installs the app, and on first launch the app routes to the original target instead of a generic home screen. #### Why did Firebase Dynamic Links stop working? Google deprecated Firebase Dynamic Links and shut the service down on 25 August 2025. Existing links stopped resolving, so any product that relied on it needs a replacement for deferred deep linking. #### How do App Clips enable deferred deep linking on iOS? An App Clip launches instantly from a link without a full install, captures the invocation URL, and writes it to a shared App Group container. After the user installs the full app, the app reads that stored link on first launch and routes the user to the original destination. #### Is the Google Play Install Referrer API reliable for deferred deep links? Yes. The Install Referrer API is an official Google API that delivers the referrer string attached to a Play Store link to the app on first launch. It is deterministic, unlike device fingerprinting, so the deep link payload arrives intact. #### Can I delay a deep link until after the user logs in? Yes. Store the resolved link in a pending queue and execute the navigation only after the required business event fires, such as a successful login. This lets you route users to authenticated screens that would otherwise fail on a cold start. ## You Have a Telegram Bot With 50k Users — Here's How to Ship It as an App in 6 Weeks https://nerdy.pro/blog/telegram-bot-to-app-6-weeks **TL;DR.** A Telegram bot with a real user base and a backend behind it can become a shipped iOS and Android app in about six weeks — not because the work is trivial, but because the hard product question (does anyone want this?) is already answered. This post is the week-by-week plan: what gets built when, where the six weeks actually go, how you migrate 50,000 users without losing them, and the specific things that turn six weeks into four months. If you want to self-qualify first, the [chatbot-to-app service page](https://nerdy.pro/services/chatbot-to-app) has a readiness checklist. If you just want the schedule, jump to [the six-week plan](#the-six-week-plan-week-by-week). --- ### The setup Picture the position you're in — it's a good one. You launched a Telegram bot a year or two ago to test an idea. It worked. You now have 50,000 users who open it, use it, and occasionally pay for it. The bot does something real: a fitness coach, an AI assistant, a finance tracker, a language tutor, a marketplace — the category doesn't matter for this post. What matters is that you have **product-market fit on a platform you don't own**. And the platform is starting to hurt. Users ask for things the bot can't do. You can't send a push notification without it landing in a muted channel. You can't charge the way you want to. A [Telegram Bot API](https://core.telegram.org/bots/api) change broke a flow last quarter and you spent a weekend firefighting. You're successful *despite* the platform, and you can feel the ceiling. So you want an app. The question every founder in this position asks is the same: *how long, and how do we not lose the 50k users we already have?* The honest answer for a large class of bots is **six weeks to a launched app**, plus a gradual migration that runs in parallel and finishes when you decide it's done. Here's why that number is realistic, and exactly where the time goes. > The walkthrough below is a composite — it's the plan we've run, in pieces, across real chat-heavy Flutter builds like [Arcana](https://nerdy.pro/portfolio/arcana) (a streaming AI chat client shipped in three weeks) and [YouMi](https://nerdy.pro/portfolio/youmi) (a real-time messaging app we stabilized in two). No single client is "the 50k bot," but every step here is something we've actually done. --- ### Why six weeks is realistic (and when it isn't) Six weeks sounds aggressive until you remember what you're *not* doing. You're not discovering the product. You're not running user research to figure out what to build — your bot is two years of user research. You're not designing a novel interaction model — the conversational flow already works and you're going to keep most of it. You're not validating demand — 50,000 people already validated it. What's left is **execution against a known specification**, and that's the part that compresses. A single [Flutter](https://flutter.dev) codebase ships to both iOS and Android at once (we've written about [why one codebase beats two native builds](https://nerdy.pro/blog/flutter-vs-react-native-2026)), so you're not paying for the same work twice. The backend mostly exists. The feature list is finite and already battle-tested in production. Six weeks is realistic when: - Your bot already talks to a **backend API**, not just Telegram's servers. - The feature set is **chat-centric plus a handful of screens** — not a sprawling super-app. - You have **one decision-maker** who can approve designs and scope without a committee. - You're willing to launch a focused v1 and add the long tail *after* the app is live. Six weeks is *not* realistic when the bot has no separate backend and all logic lives inside bot handlers (add 2–3 weeks to extract an API), when you need a from-scratch custom design system, or when "parity" secretly means fifty edge-case commands that each need their own screen. We'll come back to these in [what blows the timeline](#what-turns-six-weeks-into-four-months). --- ### The six-week plan, week by week Here's the schedule we run. The weeks overlap more than this linear list suggests — design starts while the audit finishes, store paperwork starts on day one — but the critical path looks like this. #### Week 1 — Audit and feature mapping We go through the bot command by command, flow by flow, and write down everything it does. Every `/command`, every inline keyboard, every multi-step conversation, every integration, every payment path. This becomes the specification, and it's the single most important artifact in the whole project. Then we map each bot capability to its app equivalent and sort it into three buckets: - **Keep as chat.** The conversational core stays conversational — that's what your users know. - **Promote to a screen.** Things you crammed into chat because the bot forced you to (settings, history, dashboards, profiles) become real screens. - **Cut from v1.** The commands 2% of users touch. They go on the post-launch list, not the critical path. That third bucket is where six weeks is won or lost. The output of week one is a signed-off scope: a specific list of what ships in v1. #### Week 2 — Architecture and the API question The app needs to talk to a backend. The best case is that your bot already does, and the app simply calls the same endpoints. We design the client architecture around your existing API — auth, data models, real-time channels — with no backend rewrite. If your bot has **no separate backend** (all logic inside Telegram handlers), this is where we extract a thin API layer so both the bot and the app can share it. That's the one situation that legitimately adds time, and we flag it on day one rather than discovering it in week four. This week also nails down the two subsystems that quietly sink chat apps: - **Real-time messaging** — proper [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) lifecycle: reconnection, message queuing, state sync. Getting this wrong is exactly what we were called in to fix on [YouMi](https://nerdy.pro/portfolio/youmi), where users were dropping mid-conversation. It's unglamorous and it's the difference between an app that works and one that works *when nothing goes wrong*. - **Push notifications** — [APNs](https://developer.apple.com/documentation/usernotifications) and [FCM](https://firebase.google.com/docs/cloud-messaging) wired up early, because notifications are half the reason you're leaving Telegram. #### Weeks 3–4 — Build the core This is the heaviest stretch: the actual app. In a single Flutter codebase, the team builds: - The **chat interface** — smooth scrolling over thousands of messages, rich media, streaming responses if your bot is AI-backed (the incremental-Markdown rendering we built for [Arcana](https://nerdy.pro/portfolio/arcana) lives here). - **Authentication** — phone, email, or social login, mapped to your existing user identities so accounts carry over. - The **promoted screens** from week one — dashboards, history, settings, profiles. - **Push notifications** end to end, on your schedule and your terms. You get a demo build at the end of each week. Not a status email — an installable build on a real device. This is non-negotiable: weekly builds are how scope creep gets caught while it's cheap to fix. #### Week 5 — Migration plumbing and store prep Now the part unique to *migrating* rather than building fresh. Three things run in parallel: - **Account linking.** A user opens the app and proves they're the same person who used the bot — usually a one-tap deep link from the bot carrying a signed token, or a phone/email match. Their history and data come with them. - **Store submission.** App Store and Google Play listings, screenshots, privacy declarations, review submission. [Apple's review queue](https://developer.apple.com/app-store/review/guidelines/) is the one external dependency you can't fully control, which is why this starts now, not in week six. - **The in-bot bridge.** The bot itself becomes your best acquisition channel. We add an announcement flow and deep links so the 50k users hear about the app *inside the tool they already open every day*. #### Week 6 — Launch and the start of migration The app goes live. But "launch" doesn't mean a hard cutover — and this is the part founders worry about most, so let's be precise. **You do not shut the bot off.** The bot keeps running. On launch day it starts nudging users toward the app with a deep link, but anyone who ignores it keeps using the bot exactly as before. Nobody is locked out, nothing breaks, and your 50k users don't churn because of a forced migration. Migration becomes a dial you turn over the following weeks: a banner, then a softer prompt, then app-only features that pull people across on their own. Many products keep a permanently simplified bot as a second entry point. You decide the pace; the app being live on day one is what week six delivers. --- ### How you don't lose 50,000 users This deserves its own section because it's the real fear, and the mechanics are straightforward once you see them. 1. **Parallel running, not a cutover.** Bot and app coexist. There is never a moment where a user's only option is "download the app or lose access." 2. **The bot is the migration channel.** You're not buying ads to find these users — they're already talking to you every day. A deep link in the bot converts far better than any cold install campaign, and it's free. 3. **Accounts and history carry over.** Deep-link tokens or phone/email matching mean a user signs in and finds their data already there. The app feels like an upgrade, not a fresh start. 4. **Gradual incentives, not force.** App-only features, better notifications, and a nicer experience pull users across. Pushing them with a hard deadline pushes some of them out entirely. Done this way, migration isn't a risky event — it's a curve you control, and the bot stays as a safety net the whole time. --- ### What turns six weeks into four months Honesty about the failure modes, because they're predictable: - **No backend to talk to.** If every bit of logic lives inside Telegram handlers with no API, extracting one adds 2–3 weeks. Worth doing — it's the foundation everything else stands on — but it's real time. - **"Parity" means everything.** Insisting v1 ship all 60 commands, including the ones 2% of users touch, is the most common timeline killer. Ship the core, add the tail after launch. - **Design from a blank page.** A bespoke design system invented during the build adds weeks. A clean, conventional design applied to a known flow does not. - **Decision-by-committee.** Six weeks assumes someone can approve a screen the day they see it. If every decision waits a week for a meeting, the calendar — not the engineering — sets the pace. - **A rescue, not a build.** If there's an existing half-finished app that's breaking, fixing inherited problems is a different job than starting clean — that's the [YouMi](https://nerdy.pro/portfolio/youmi) situation, and it's scoped separately. None of these are mysteries. We surface them in week one, which is the entire point of starting with an audit instead of a Gantt chart. --- ### What it costs A straightforward migration on this timeline — core features preserved, chat interface, auth, push notifications, store deployment, and user migration — lands in the **$5,000 – $10,000** range over four to six weeks. Feature-rich bots with payments, offline support, and custom screens run higher and longer; AI-backed bots with streaming and real-time rendering higher still. The full breakdown by tier is on the [chatbot-to-app service page](https://nerdy.pro/services/chatbot-to-app), and the [cost framework](https://nerdy.pro/blog/flutter-app-development-cost-2026) explains how scope compounds into the final number. The thing worth internalizing: the expensive part of building an app is finding product-market fit, and **you already paid that bill** with your bot. The migration is the cheap part. You're converting validated demand into a product you own — and that's a far better starting position than the founder staring at a blank Figma file, guessing what to build. --- ### Frequently asked questions #### How long does it take to turn a Telegram bot into a mobile app? A straightforward bot with a working backend can ship as an iOS and Android app in about six weeks. Feature-rich bots with payments, offline support, or AI streaming typically take two to four months. The timeline depends mostly on whether the bot already has a separate backend API and how much of the feature set must ship in version one. #### Will I lose my users when I migrate from the bot to the app? Not if you migrate gradually. The bot keeps running alongside the app, so no user is ever locked out. The bot itself becomes the migration channel: a deep link inside it converts far better than paid ads, and accounts plus history carry over, so the app feels like an upgrade rather than a fresh start. #### Do I need to rebuild my backend to launch the app? Usually not. If your bot already talks to a backend API, the app calls the same endpoints with no rewrite. Only when all logic lives inside Telegram handlers with no separate backend do you need to extract a thin API layer first, which adds about two to three weeks. #### How much does it cost to convert a Telegram bot into an app? A straightforward migration with core features, chat, authentication, push notifications, and store deployment lands around 5,000 to 10,000 US dollars. Feature-rich and AI-backed bots cost more. Because the bot already proved demand, the most expensive part of building an app is already paid for. #### Can the Telegram bot keep running after the app launches? Yes. Most products keep a simplified bot running permanently as a second entry point. Launch is not a hard cutover: the bot nudges users toward the app over time while staying fully functional for anyone who has not switched yet. #### Why build the app with Flutter? Flutter ships to iOS and Android from a single codebase, which cuts time and cost by roughly 30 to 40 percent versus two native builds, with no performance penalty. It is especially strong for chat-heavy apps that need smooth scrolling, real-time updates, and rich media rendering. :: ### Start with the checklist Before a single line of code, run your bot through the [migration readiness checklist](https://nerdy.pro/services/chatbot-to-app#bot-to-app-migration-readiness-checklist) on the service page. It tells you, honestly, how close to a clean six-week migration you actually are — and where the gaps will cost you time. If you've got a bot with real users and you're feeling the platform ceiling, [book a discovery call](https://nerdy.pro/contact). We'll walk your bot through the audit, tell you which weeks will be easy and which will be hard, and give you a fixed-scope quote — not a guess. ## What a 40% Mobile Cost Reduction Actually Looks Like — Numbers from 4 Nerdy.pro Projects https://nerdy.pro/blog/mobile-cost-reduction-case-studies **TL;DR.** "Cut mobile costs by 40%" is the kind of number that shows up in agency pitch decks and means almost nothing without the breakdown. This post is the breakdown. We pull four projects from our portfolio — [Arcana](https://nerdy.pro/portfolio/arcana), [YouMi](https://nerdy.pro/portfolio/youmi), [ExtraETF](https://nerdy.pro/portfolio/extraetf), and [Formtastic](https://nerdy.pro/portfolio/formtastic) — and show exactly where the cost reduction came from in each one. Some of it was framework choice. Some of it was *not* having to rebuild. None of it was magic. If you want the cost levers without the case studies, jump to [the five drivers](#the-five-drivers-behind-every-cost-cut). --- ### Why "40% cheaper" is usually a lie Whenever an agency tells you they reduced a client's mobile cost by some round percentage, ask one question: **40% of what, measured against what?** - 40% off the **hourly rate** is offshoring, not engineering. - 40% off the **headcount** is firing people, not optimizing the build. - 40% off the **time-to-market** is a real engineering win, because it compounds — every week of TTM savings is a week of revenue, a week of feedback, a week of competitive position. - 40% off the **total cost of ownership** over three years is the rarest and most valuable, because it includes the maintenance bill nobody quotes upfront. The four case studies below are all real, all in our portfolio, all verifiable. They illustrate different cost levers, and the savings come from different places. None of them is "we just used cheaper engineers." --- ### Case 1 — ExtraETF: the 40% time-to-market reduction **Project:** [ExtraETF](https://nerdy.pro/portfolio/extraetf) for Isarvest GmbH. Duration: 6 months. Stack: Flutter + Go. ExtraETF is a financial app for ETF and stock analysis — real-time price streams, interactive charts, push notifications, OAuth, in-app subscriptions. The kind of app where the temptation is to build two native codebases (Swift on iOS, Kotlin on Android), because financial apps "have to feel native." We didn't. We built it once in Flutter and shipped it to both stores in 6 months with feature parity. The estimated TTM reduction versus a parallel native build was **40%**. Where did the 40% actually come from? - **A single mobile codebase instead of two.** Most native builds run iOS and Android as parallel tracks. Even with the best coordination, you double-write business logic, you fix the same bug twice, and you ship features that work on one platform but break on the other. Flutter eliminates that overhead entirely. - **No platform-divergence backlog.** When iOS ships a feature first, Android customers wait. When Android catches up, iOS has shipped two more. The platform-divergence backlog is a hidden tax — Flutter doesn't have one. - **Charting work done once.** ExtraETF is chart-heavy. Native charting libraries on iOS and Android are completely different beasts; matching their look and behavior costs weeks. Flutter draws every pixel itself, so the chart code runs identically on both platforms. The 40% TTM number isn't a marketing line — it's what falls out of the math when you stop paying for the same work twice. The Go-based WebSocket service for live prices is a side note here; that lever was about backend cost-per-connection at scale, not mobile cost. **Cost lever:** Single codebase across iOS and Android. Verifiable, repeatable, not a one-time trick. --- ### Case 2 — Formtastic: consolidating two native apps into one **Project:** [Formtastic](https://nerdy.pro/portfolio/formtastic) for Formtastic GmbH. Duration: 1 year (full platform evolution). Stack: Flutter + Nuxt + Django + Go. Formtastic is the *other* shape of cost reduction. They already had two native mobile apps in production — one iOS, one Android — and the cost wasn't in *building* mobile, it was in **maintaining** mobile. Two codebases, two teams, two release cycles, two QA passes for every feature. We migrated both apps into a single Flutter codebase. The mobile cost reduction here wasn't a percentage off a quote — it was a structural shift in how much it cost to operate the product month over month. What changed concretely: - **Release cycles synchronized.** Before: iOS shipped, Android shipped a week later if the team was lucky. After: one build, one release, both stores at once. - **Feature parity stopped being a coordination problem.** A new feature is written once. Edge cases are handled once. The "we forgot to add this to Android" Slack message disappeared. - **The maintenance line item shrank.** Two native apps require two sets of upgrade work every year — new iOS version, new Android version, new SDK requirements, new App Store policies, new Play Store policies. Flutter still has upgrade cost, but it's roughly half. In parallel, the web frontend moved from Django templates to a separate Nuxt SPA, and a handful of hot-path services moved from Django to Go. Both decisions had their own ROI, but the mobile consolidation was the largest single line item on the cost-reduction ledger. **Cost lever:** Reducing surface area. Two codebases → one. Two teams → one. Most cost-cutting projects we see make the mistake of trying to do the same work cheaper. Formtastic did less work, at the same quality. --- ### Case 3 — YouMi: the price of not having Flutter expertise **Project:** [YouMi](https://nerdy.pro/portfolio/youmi) for YouMi LLC. Duration: 2 weeks. Stack: Flutter. YouMi is the case study founders most want to ignore, because it's about the cost of *not* having the right team in the first place. YouMi already had a Flutter app in production. It was breaking. Users were getting disconnected mid-session with their psychologists. Chat messages were dropping. Navigation between screens was unpredictable. The product worked when nothing went wrong, which in a real production app is never. The root causes were two unglamorous things: a WebSocket layer with no proper lifecycle management, and a navigation stack with memory leaks. Both are problems a Flutter engineer who has shipped a few production apps solves on autopilot. Both are problems a Flutter engineer who has *not* shipped a few production apps will struggle with for months. We took the codebase, fixed both subsystems, and shipped a stable build in **two weeks**. Here's the cost framing: the alternative wasn't "build it cheaper." The alternative was "keep losing users until the product fails." Calculate the cost of that — the churn, the refunds, the brand damage, the founder's morale — and the 2-week engagement is not a line item in the project budget. It's the project surviving at all. This is the wedge for Team Augmentation as a service: a small, embedded engineer with the right pattern recognition prevents the kind of slow-motion product failure that has no line item in any quote. **Cost lever:** Pattern recognition. The cheapest version of any mobile project is the one that doesn't need a rescue six months in. --- ### Case 4 — Arcana: speed-to-prototype on a hard problem **Project:** [Arcana](https://nerdy.pro/portfolio/arcana) for an AI-tarot client. Duration: 3 weeks. Stack: Flutter. Arcana is what speed-to-prototype looks like when the technical problem is genuinely hard. The client owned the AI backend. They needed a chat client that could: - Stream AI responses in real time via Server-Sent Events. - Render Markdown formatting incrementally as tokens arrive — bold, italics, headers, lists — without flicker or layout jank. - Stay smooth at 60 fps on a low-end Android device while scrolling through 5,000 messages. That's not a CRUD app. That's a custom rendering engine bolted to a custom networking layer. We shipped it in 3 weeks. The cost lever here is subtle. We didn't save the client money by writing less code — building this in 3 weeks required *more* careful engineering than building a typical chat app in 3 months. We saved them money by collapsing the schedule. Every week the AI product wasn't in front of users was a week of competitor traction, investor questions, and team uncertainty. A 3-week build let them validate the product hypothesis before the runway math turned ugly. Speed-to-prototype is a cost lever that doesn't show up in any per-hour calculation. It shows up in *which decisions you get to make*, and *how many you get to make before you run out of money*. **Cost lever:** Time. Specifically, decision-making time bought back by shipping fast on a hard problem. --- ### The five drivers behind every cost cut Across these four projects, the cost reductions came from the same five repeatable drivers. None of them is exotic. All of them require an agency willing to be honest about scope. #### 1. Single codebase, not two The cheapest cross-platform mobile build is the one that genuinely runs on both platforms from one source — the premise of our [Flutter app development](https://nerdy.pro/services/flutter-app-development) work. Flutter's claim to this is stronger than React Native's for UI-heavy products (we explored why in [Flutter vs React Native in 2026](https://nerdy.pro/blog/flutter-vs-react-native-2026)), but the bigger point is that **any** real cross-platform stack beats two parallel native builds on cost. Most founders underestimate how much of a native build is duplication. #### 2. Reducing surface area, not cutting corners The Formtastic pattern. The cheapest feature is the one you don't have to maintain in two places. The cheapest backend is the one with fewer services, not more. Cost reduction by simplification compounds; cost reduction by cutting QA does not. #### 3. Pattern recognition over hours The YouMi pattern. A senior Flutter engineer on a familiar problem class is dramatically faster than two mid-level engineers on the same problem. The hourly rate is higher; the total cost is lower. This is the entire premise of Team Augmentation — embed expertise that prevents the expensive failure modes, not just hands that execute tickets. #### 4. Speed-to-prototype on novel problems The Arcana pattern. When the technical problem is unprecedented for the team, the cost lever isn't reducing the build — it's reducing the time between "we have a hypothesis" and "we have data." Three weeks vs. three months isn't a 75% cost saving on engineering; it's the difference between a product that ships and a product that runs out of runway. #### 5. Lower long-term maintenance The hidden line item. Every mobile framework choice is a 3-to-5-year commitment. The upgrade and maintenance bill over that window is often larger than the initial build. We've written about the per-tier cost breakdown in [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026), but the short version: choose the stack with the lowest *future* cost, not the lowest *today* cost. --- ### What this means for your project If you're a founder reading this with a mobile project in your head, the question isn't "how do I get 40% off?" The question is which of the five levers actually applies to your situation: - If you're greenfield and planning two native builds — lever #1. - If you're already paying to maintain two native apps — lever #2. - If your existing app is breaking and you don't have a senior Flutter engineer on staff — lever #3. - If you're racing a competitor or a runway clock — lever #4. - If you're building something you intend to operate for the next 3+ years — lever #5. Most projects have one or two of these. A few have three. The case studies above each leaned hard on a different one, which is why the cost reductions looked different from the outside even though the underlying mechanics were the same. --- ### Estimate your own project The most reliable way to get a real number for your specific project is the slowest one: a 30-minute call where we look at the five drivers against your actual situation. No deck, no pitch — just the conversation that produces an honest quote. If that's useful, [book a discovery call](https://nerdy.pro/contact). If you want to read the framework first, [Flutter App Development Cost in 2026](https://nerdy.pro/blog/flutter-app-development-cost-2026) is the companion post that walks through how scope, backend, integrations, design, and compliance compound into the final number. And if your product started as a Telegram bot, [shipping it as an app in six weeks](https://nerdy.pro/blog/telegram-bot-to-app-6-weeks) walks through that specific migration — timeline, cost, and how not to lose your users. And if you've already got a Flutter app in the wild that's misbehaving — like YouMi was — that's the conversation we'd most like to have first. Two weeks of senior engineering at the right time is almost always cheaper than two more quarters of slow-motion failure. ## Flutter vs React Native in 2026: The Honest Engineering Tradeoffs https://nerdy.pro/blog/flutter-vs-react-native-2026 **TL;DR.** In 2026, both Flutter and React Native are production-grade. Neither "wins." The right choice is determined by your team, your UI requirements, how many surfaces you need (mobile only? mobile + web? desktop? embedded?), and how your hiring pipeline looks. We build in Flutter at [Nerdy.pro](https://nerdy.pro/) because the economics work for our clients — but we've shipped real React Native work too, and this post is our honest comparison of where each one earns its place. If you just want the answer: skip to [the decision framework](#the-short-decision-framework). If you want the engineering depth behind it, keep reading. Comparing Flutter to **native** iOS/Android (Swift, Kotlin) rather than to React Native? That's a separate decision — we cover it in [Flutter vs Native in 2026](https://nerdy.pro/blog/flutter-vs-native-2026). --- ### Why we wrote this post (and why it's different) Search "flutter vs react native 2026" and you'll find mostly the same listicle, rewritten a hundred times: a feature-matrix table, a handful of out-of-date talking points about "the JS bridge," and a recommendation suspiciously aligned with whichever framework the author's agency sells. We're a Flutter-first agency. We are not neutral. But we've used React Native enough — on production apps with real users — to tell you when it's the right answer. This post exists because our clients deserve a decision, not a sales pitch, and because the honest answer is more useful than the tribal one. Where we give an opinion, it's grounded in work we've actually shipped. Four of those apps are public: [Arcana](https://nerdy.pro/portfolio/arcana), [YouMi](https://nerdy.pro/portfolio/youmi), [ExtraETF](https://nerdy.pro/portfolio/extraetf), and [Formtastic](https://nerdy.pro/portfolio/formtastic). We'll refer to them below. --- ### What actually changed between 2023 and 2026 Most comparison posts are still arguing about things that got fixed. Before we compare anything, here's what you need to know about the current state of both frameworks. **React Native's New Architecture is the default now, not an experiment.** Fabric (the new renderer), TurboModules (the new native module system), and the Bridgeless mode shipped in React Native 0.76 as defaults in late 2024. The "JS bridge bottleneck" argument that dominated 2020–2023 discourse is largely obsolete. Modern RN calls into native code synchronously through JSI, and the serialization overhead people used to complain about is gone on any project that has adopted the New Architecture. If your reference points are Airbnb's 2018 blog post or Discord's early criticisms, throw them out. **Flutter's Impeller is no longer the "new" renderer.** Impeller replaced Skia as the default on iOS in 2023 and became the default on Android in 2024. It eliminated the shader-compilation jank that was Flutter's most visible production problem for years. If your last serious look at Flutter was 2022, the rendering story has changed materially. **Expo is effectively the standard way to ship React Native now.** The "bare React Native CLI" workflow still exists, but in practice, new RN projects are Expo projects. Expo Router, EAS Build, and Expo's prebuild system have made RN's previously painful DX much closer to Flutter's. Comparisons that don't mention Expo are comparing against a version of RN that 2026 teams rarely use. **Flutter on web, desktop, and embedded is real, but nuanced.** Flutter Web is production-ready for internal tools, admin panels, and app-like experiences — not for content sites or anything SEO-dependent. Flutter desktop (macOS, Windows, Linux) is stable. Flutter embedded has a real ecosystem (cars, kiosks, appliances). React Native's web story (via react-native-web, Solito, or Expo Router's web output) is also real but remains community-led rather than core. **The hiring landscape has narrowed the gap.** In 2022, "there are 10× more React devs than Dart devs" was the standard counter-argument against Flutter. That's still directionally true, but the Flutter talent pool has matured dramatically. For senior roles — the people actually setting architecture — both frameworks have plenty of qualified engineers. With that baseline set, let's compare. --- ### Eight dimensions that actually matter #### 1. UI fidelity and design control Flutter paints every pixel itself. React Native renders platform-native views (UIView on iOS, Android Views or Compose interop on Android). **What this means in practice:** - If your design system is bespoke — custom animations, non-standard controls, pixel-perfect identical look across iOS and Android — Flutter will be dramatically faster to implement. This was decisive for [Arcana](https://nerdy.pro/portfolio/arcana), where the UI intentionally doesn't conform to either platform's conventions. - If your product needs to feel *native* — system-level context menus, native iOS navigation transitions, exact platform accessibility semantics — React Native has a real advantage. A banking app or a health app that users expect to feel like a "normal" iOS app will be easier to get right in RN. **Honest call:** RN wins if platform-native feel is a brand value. Flutter wins if design-system consistency is a brand value. Neither wins if you don't have a strong opinion. #### 2. Performance and rendering architecture With both frameworks on their modern architectures (Impeller for Flutter, Fabric + Bridgeless for RN), raw rendering performance is close enough that **most teams will not notice a difference in a typical CRUD app.** Where the gap still shows up: - **High-frequency animation and custom drawing** — Flutter's architecture (directly painting on the GPU, no reconciliation through a native view tree) still wins. If you're building a drawing app, a map-heavy product, a charting library, or a game-adjacent UI, Flutter is the right tool. We used this argument to choose Flutter for [ExtraETF](https://nerdy.pro/portfolio/extraetf), where the chart-heavy investor UI would have been painful in RN. - **Startup time on low-end Android** — Flutter historically had a larger binary size and slower cold start. That's mostly solved in 2026, but on sub-2GB-RAM devices in emerging markets, RN with Hermes still tends to boot faster. - **Native module hot path** — RN's JSI now makes sync native calls cheap. For apps whose performance is dominated by many small native calls (heavy hardware integration, IoT, BLE control), RN is measurably simpler. **Honest call:** Flutter is the safer choice for animation-heavy or graphically complex UIs. RN is the safer choice if you're on tight binary-size budgets or have lots of native-module chatter. #### 3. Platform breadth This is where Flutter is simply ahead in 2026. - **Mobile (iOS + Android)** — parity. - **Web** — Flutter Web is core; RN Web exists as a community effort (react-native-web). If web is a first-class surface for you and you're not willing to maintain a parallel React codebase, Flutter is easier to reason about. If web is your *primary* surface and mobile is an afterthought, neither is the right answer — you want Next.js or Remix with native wrappers. - **Desktop (macOS, Windows, Linux)** — Flutter is production-ready. RN-macOS and RN-Windows exist and are maintained by Microsoft, but are behind Flutter in library ecosystem completeness. - **Embedded (cars, kiosks, appliances)** — Flutter has the only real story here. Toyota, BMW, Canonical, and others ship Flutter on embedded hardware. RN does not target this space. **Honest call:** If you need more than iOS + Android, Flutter. If iOS + Android is the whole roadmap forever, neither platform has a breadth advantage. #### 4. Ecosystem and libraries React Native's ecosystem is larger and more mature in absolute terms. The npm ecosystem is JavaScript's, and RN inherits it. Practical implications: - **SDK coverage** — Payment providers, analytics tools, auth services, CRM integrations almost always ship an RN SDK. Some ship a Flutter SDK too, but RN is the safer bet if your app's value lives in third-party integrations. - **Component libraries** — RN has more off-the-shelf UI kits (especially tied to React's Web ecosystem). Flutter's component library is more curated but smaller in raw count. - **Package quality variance** — Both ecosystems have quality issues in the long tail. Flutter's pub.dev has better typing and stronger null-safety guarantees than a typical npm package. If you've been burned by unmaintained RN libraries, that's a real concern. **Honest call:** RN wins on ecosystem breadth. Flutter wins on ecosystem consistency. #### 5. Hiring, team cost, and ramp-up This is where most decisions are actually made, even if the technical arguments get more airtime. - **Pool size** — More JavaScript/React engineers exist than Dart engineers. This is structural and won't change. - **Ramp-up from web** — A React developer can contribute to an RN codebase in days. A Flutter codebase requires learning Dart (easy) and Flutter's widget model (takes a few weeks to stop fighting it). - **Senior talent** — For senior roles, the pool-size argument weakens. Good senior mobile engineers are rare in both ecosystems. - **Agency pricing** — In our experience pricing projects, Flutter agencies and RN agencies quote within 10% of each other for comparable scope. The cost difference is almost entirely ecosystem-dependent (see dimension 4) and project-specific, not framework-intrinsic. For a detailed breakdown of how these variables translate into actual project cost, see our companion post: [Flutter App Development Cost in 2026: Real Numbers from Real Projects](https://nerdy.pro/blog/flutter-app-development-cost-2026). **Honest call:** If you already have a React team, the ramp cost for RN is near-zero. Don't retrain them on Dart just to use Flutter. If you're hiring fresh, the choice is closer than pool-size arguments suggest. #### 6. Developer experience and tooling Close, but with different flavor. - **Hot reload** — Both are fast. Flutter's is still marginally quicker and more reliable in our experience, especially after state-heavy changes. - **Build system** — Expo (for RN) has closed most of the historical gap. A greenfield Expo project is genuinely pleasant. A bare-RN project is still rougher than a Flutter project at day one. - **Language** — Dart is a small, predictable language. TypeScript is more powerful but more complex. This is personal preference; neither is clearly better for shipping apps. - **Tooling maturity** — Flutter's IDE tooling (via `flutter doctor`, DevTools, and the Dart analyzer) is tightly integrated and usually the source of truth. RN's tooling depends more on which layers you pick (Metro, Hermes, Expo, Reanimated, etc.), which is more powerful but requires more judgment. **Honest call:** Flutter's DX is more opinionated and a bit smoother out of the box. RN's DX is more configurable and rewards teams that know what they want. #### 7. Long-term maintenance and upgrade pain A cross-platform framework is a 3-to-5-year decision. Upgrade cost matters more than most founders expect. - **Flutter** — Breaking changes are rare and well-signposted. Two-year-old Flutter codebases upgrade cleanly in a day of work. - **React Native** — Historically upgrading RN was painful (the "Upgrade Helper" became famous for the wrong reasons). The New Architecture migration, now largely complete, was a multi-quarter undertaking for large teams. Expo has smoothed the upgrade path significantly, but RN still requires more upgrade discipline than Flutter. **Honest call:** Flutter is the lower-maintenance choice over a 3-year horizon. The gap narrows if you stay on Expo's managed workflow. #### 8. App Store review and platform-policy edge cases Less famous, but expensive when it bites. - **Apple 4.2.6 (the "commercialized template" rule)** — Bites both frameworks equally. Neither framework triggers 4.2.6 on its own; it's triggered by how apps are differentiated from each other. Relevant mostly if you're running a white-label or multi-tenant strategy. - **Binary size** — RN apps tend to ship smaller than Flutter apps on Android. Usually not decisive, but a real consideration for apps with aggressive install-conversion targets. - **Accessibility** — RN inherits the platform's native accessibility tree, which makes it easier to pass accessibility audits in enterprise sales. Flutter has its own accessibility layer that's good but requires more explicit care. **Honest call:** For enterprise-sold products with strict accessibility or binary-size requirements, RN starts slightly ahead. For everything else, it's a wash. --- ### Pick Flutter if… - You want pixel-perfect UI consistency across iOS and Android (and often web + desktop). - Your product is UI-heavy, animation-heavy, or does custom rendering (charts, canvases, games, drawing tools). - You need more than mobile — web, desktop, or embedded are on the roadmap. - You value low upgrade and maintenance cost over 3–5 years. - You don't already have a React team. This is why most of the projects in our [portfolio](https://nerdy.pro/portfolio) are built in Flutter — it's the core of our [Flutter app development](https://nerdy.pro/services/flutter-app-development) practice. ### Pick React Native if… - You already have a React or JavaScript team and want to minimize retraining. - Your product needs to feel *native* — banking, healthcare, system utilities, or any context where users expect conformity to platform conventions. - Your app's value lives in third-party SDKs (payments, analytics, niche integrations) and you want the largest possible library surface. - You want to share code with a React web app without maintaining two codebases. - You ship primarily on Android with tight install-conversion KPIs and binary size is a measured concern. --- ### The short decision framework If you want one paragraph, here it is. **Pick React Native if your team is already React, your product should feel native, or your value lives in third-party SDKs. Pick Flutter if you own your design system, your roadmap extends beyond iOS + Android, or you want the lowest maintenance cost over the next three years.** If both halves of that sentence apply, the tiebreaker is team composition: use what your senior engineers will be fastest in. --- ### Cost implications Framework choice influences cost, but usually less than product scope, design complexity, and integration count. The same MVP built well in either framework will land within ±10% of the other in our pricing. Where the cost gap becomes real is the long tail: maintenance, upgrade pain, and the engineering time spent wrapping native SDKs that don't ship a first-class package for your chosen framework. We wrote a separate post dissecting this with numbers from our own projects: [Flutter App Development Cost in 2026: Real Numbers from Real Projects](https://nerdy.pro/blog/flutter-app-development-cost-2026). --- ### How we approach the decision at Nerdy.pro When a new client asks us whether they should use Flutter or React Native, we run a 30-minute call where we look at: 1. **Who is on your team today, and who will maintain this in two years?** 2. **What surfaces do you need to ship on — mobile only, or mobile + web + desktop?** 3. **What are your three largest third-party integrations, and which frameworks do they support?** 4. **How platform-native does the product need to feel?** 5. **What's your timeline, and what's your cost ceiling?** If the answers strongly favor React Native, we say so and help you find a good RN shop. If they favor Flutter, we quote the project. If they're ambiguous — which is common — we build a small proof-of-concept in whichever framework the team is more likely to maintain. If you'd like us to run this exercise on your product, [book a discovery call](https://nerdy.pro/contact). No slide deck, no pitch — just a technical call to get the decision right. You can also see how this thinking plays out in practice by browsing [our portfolio](https://nerdy.pro/portfolio): [Arcana](https://nerdy.pro/portfolio/arcana), [YouMi](https://nerdy.pro/portfolio/youmi), [ExtraETF](https://nerdy.pro/portfolio/extraetf), and [Formtastic](https://nerdy.pro/portfolio/formtastic) each chose Flutter for a specific reason that we'd be happy to walk through on a call. --- ### FAQ #### Is React Native dead in 2026? No. Meta continues to invest in it, the New Architecture shipped as default in 2024, and Expo has made the developer experience substantially better. React Native is growing, not declining. Anyone telling you otherwise is selling you something. #### Is Flutter still slower to start than native on Android? Cold-start latency on low-end Android devices is Flutter's remaining weak spot. On mid-range and flagship devices, startup time is indistinguishable from native in a well-built app. If sub-2GB-RAM Android devices in emerging markets are a primary user segment, benchmark before committing. #### Should I use Flutter for a content-heavy website? No. Use Next.js, Remix, or another HTML-first framework. Flutter Web paints on canvas, which is bad for SEO and accessibility on content sites. Flutter Web is excellent for app-like experiences (dashboards, admin panels, interactive tools), not for blogs or marketing sites. #### Can I migrate from React Native to Flutter (or vice versa)? Yes, but it's effectively a rewrite. There are no meaningful shared artifacts between the two stacks. Budget it as a greenfield project, not a migration, and ask whether the switch solves a real problem before committing. In most cases, the pain of your current framework is not framework-level — it's architectural, and rewriting into the other framework just buys you the same architectural problems in a different language. #### What about Kotlin Multiplatform or Swift-based cross-platform? KMP is a credible third option for teams that want native UI on each platform while sharing business logic. It's narrower in scope than either Flutter or RN and is most compelling for organizations that already have strong Kotlin and Swift teams. We've evaluated it for clients but haven't yet shipped production work on it. The full comparison is now its own post: [Flutter vs Kotlin Multiplatform in 2026](https://nerdy.pro/blog/flutter-vs-kotlin-multiplatform-2026). #### Does the choice matter for an MVP? Less than you think. The cost and timeline of a typical MVP (8–16 weeks) is dominated by scope decisions, not framework decisions. Pick the framework your team will ship fastest in, and revisit the choice at product-market fit when the real engineering decisions start. --- *Want us to apply this framework to your product? [Book a 30-minute call](https://nerdy.pro/contact) and we'll give you an honest recommendation, even if it means sending you to someone else.* ## Flutter App Development Cost in 2026: Real Numbers from Real Projects https://nerdy.pro/blog/flutter-app-development-cost-2026 Most articles on this topic are useless. They give you a range of $10–500K and call it a day. That range is technically correct and practically worthless — it's the software equivalent of "a house costs somewhere between $50K and $50M." This post is different. We run a Flutter agency, we've quoted a lot of projects, and we'll tell you the real numbers: what drives them, where agencies pad their estimates, and what you actually get at each price point. No ranges wider than a shipping container. The tiers below mirror the ones on our [Flutter app development](https://nerdy.pro/services/flutter-app-development) page. The first of them has a page of its own: [MVP development](https://nerdy.pro/services/flutter-app-development/mvp). ### The short answer In 2026, a Flutter app costs: - **$15,000 – $30,000** for an MVP (2–3 months, small team, focused scope) - **$30,000 – $60,000** for a full-featured business app (3–5 months, custom design, admin dashboard) - **$50,000 – $90,000** for an e-commerce or retail app (4–6 months, payments, inventory, multi-role access) - **$90,000+** for enterprise or AI-driven apps (6+ months, compliance, custom infrastructure, multi-tenant) These are agency rates for a European client working with a competent European team. A US-based agency will multiply these by roughly 2–3x for comparable work. A freelancer might quote half, but the comparison isn't apples-to-apples — agencies absorb risk that freelancers push onto the client. ### What actually drives the cost The cost of a Flutter app comes down to five levers. Everything else is detail. #### 1. Scope — the biggest variable by far Most founders dramatically underestimate scope. You describe "a simple app to track workouts" and in your head it's five screens. In reality, it's: - Onboarding flow (3–4 screens) - Authentication (email, Google, Apple — each with edge cases) - Main feature (the workouts themselves) - Settings, profile, account deletion (required by both stores) - Payment and subscription flow (if monetized) - Push notifications - Offline support - Admin panel - Analytics integration - App store listings and review process That "simple app" is 40+ screens and 3 months of work minimum. Scope creep isn't a bug in the estimation process — it's what happens when real apps collide with real users. #### 2. Backend complexity Flutter is the frontend. Your costs roughly double when you need a serious backend: custom APIs, real-time features, complex data models, admin dashboards, third-party integrations. A Firebase-based MVP is cheap because Firebase handles auth, database, storage, and push notifications out of the box. The moment you need PostgreSQL, custom business logic, webhooks, or compliance requirements, you're building a backend team alongside the app team. #### 3. Integrations Every integration is a week of work you didn't plan for. Payment processors, mapping, analytics, CRM sync, calendar sync, video, chat, biometrics — each one looks like "just add the SDK" in the pitch and turns into a two-week debugging session about edge cases on Android 10. Rule of thumb: every major integration adds $3–8K to the project. #### 4. Design A custom, polished UI doubles your design budget compared to using a standard component library. Custom animations, illustration sets, dark/light themes with real attention to detail, and accessibility compliance all cost real money. They're also the difference between an app that feels cheap and one that doesn't. Good Flutter design work runs $6–20K depending on the tier, more for complex apps. Skipping design is the most common way founders make their app feel worse than it needs to. #### 5. Compliance If you're in fintech, healthcare, or anything touching regulated data, add 30–50% to every estimate above. HIPAA, PCI-DSS, GDPR with actual audit trails, SOC 2 — these aren't checkboxes, they're architectural constraints that touch every part of the codebase. A healthcare app isn't a regular app with a privacy policy. It's a regular app built on top of infrastructure that assumes auditors will read it. ### What you get at each tier #### The MVP tier: $15,000–$30,000 Small team, 2–3 months. You get: - 5–8 core features, iOS and Android - Firebase backend (auth, Firestore, maybe Cloud Functions) or a lightweight REST API - Basic UI/UX design using a component library - User authentication - Basic analytics - App store deployment - Minimal testing — you'll find bugs in production This is enough to validate a product idea and put something in users' hands. It is not enough to scale to 100,000 users. Founders who expect MVP quality to survive growth are the ones who end up rewriting the app 18 months later. #### The business app tier: $30,000–$60,000 Proper team — Flutter developers, designer, part-time backend engineer, QA. 3–5 months. You get: - 15–20 features across iOS, Android, and Web - Custom UI/UX design - Custom backend with advanced API integrations - Push notifications with segmentation - Offline functionality - Analytics and crash reporting wired up properly - Admin dashboard - Accessibility pass - App store optimization assets This is what a real business app looks like. Most funded startups operate in this tier. #### The e-commerce tier: $50,000–$90,000 Specialized because e-commerce apps share a common set of hard problems: payments, inventory, multi-role access. 4–6 months. You get everything in the business tier, plus: - Full product catalog with search and filters - Shopping cart and checkout - Payment gateway integration (Stripe, local processors, or both) - Order tracking - Customer reviews and ratings - Inventory management - Admin and vendor panels (if multi-vendor) The reason e-commerce costs more than a business app of similar scope isn't the screens — it's the edge cases. Failed payments, refunds, partial shipments, cart abandonment, tax calculation, fraud prevention. Every one of these is a feature that takes real time to build right. #### The enterprise or AI tier: $90,000+ Full team — multiple Flutter engineers, dedicated backend team, designer, QA, DevOps, project manager. 6+ months, often ongoing. You get: - Complex workflows across all platforms - AI/ML integration (LLM orchestration, on-device inference, custom model pipelines) - Enterprise security and compliance work - Custom backend infrastructure, multi-tenant architecture - Role-based access control, admin portals - Deep third-party integrations (ERP, CRM, hardware SDKs, custom protocols) - Performance work (target 60fps on low-end devices, cold start under 2 seconds) - Release trains, feature flags, staged rollouts - Dedicated support, SLAs after launch Projects above $150K usually aren't one project — they're a team. ### Where agencies pad estimates A few patterns worth knowing about, because they're almost universal. **Inflated hourly rates.** A developer costing the agency $40/hour gets billed to you at $90/hour. This is normal and fine — agencies need margin to cover sales, management, taxes, and the gaps between projects. But when the blended rate goes above $150/hour from a European shop, you're paying for brand, not capability. **Discovery as a separate line item.** "We'll do a 4-week discovery phase for $15K before we can estimate the project." Sometimes legitimate, often a way to collect money before anyone commits to anything real. Ask what specifically will be delivered and whether the output is useful if you don't proceed with them. **Buffer on top of buffer.** Good agencies add a 20–30% buffer to estimates because software is unpredictable. Bad ones add 50%+ and call it "risk management." If an estimate seems high, ask for a line-item breakdown. Reasonable agencies will provide it. **"Enterprise" pricing for MVP work.** Some shops quote $200K for what should be a $30K MVP, because they only know how to sell at one price point. If the estimate doesn't match the scope, the agency either doesn't understand the scope or doesn't want to. ### How regional rates actually compare in 2026 Rough blended agency rates we see in the market: - **US-based agencies:** $150 – $250/hour - **Western Europe (UK, Germany, Netherlands):** $100 – $180/hour - **Eastern Europe:** $50 – $95/hour - **South/Southeast Asia:** $25 – $55/hour (wider quality variance) The quality variance inside each region is larger than the variance between regions. A good Eastern European agency will outperform a mediocre US agency on Flutter work every time — Flutter has deep roots in the European developer community, and some of the most-downloaded packages on pub.dev come from teams based in this region. What you're paying for at the higher tiers isn't necessarily better code. It's better communication, stronger project management, and the reduced risk of dealing with a shop that vanishes mid-project. Those are real things to pay for, but they're not always what you need. ### A framework for budgeting your project Skip the spreadsheets. Answer these four questions: 1. **What happens if this app is late by 3 months?** If the answer is "we miss a critical window," budget for the business tier minimum. If the answer is "we'd rather know now if the idea is wrong," the MVP tier is your starting point. 2. **Who are the first 1,000 users?** If you have a list, you can validate with an MVP. If you're hoping to acquire them through marketing, you need a polished app — which means the business tier or higher. 3. **Is the business model subscription or one-time?** Subscription businesses need proper subscription infrastructure, which is more expensive than it looks. Budget 20–30% extra for billing, entitlements, and customer support flows. 4. **What's your true technical risk?** A to-do app is low-risk. A video calling app is high-risk. Work with an agency that's shipped your risk category before — we've taken on fintech with [ExtraETF](https://nerdy.pro/portfolio/extraetf) and real-time AI chat with [Arcana](https://nerdy.pro/portfolio/arcana). The $20K you save on an inexperienced team will cost $80K to fix later. ### Final thought The cheapest quote is almost never the best value. Neither is the most expensive. What you're looking for is an agency that understands your scope, tells you what it will actually cost, and is honest about what won't be included. Vague estimates with huge ranges are a red flag — they mean the agency hasn't actually thought about your project. Already validated your idea with a Telegram bot? The cost calculus is different — we walk through a typical six-week bot-to-app migration, and what it costs, in [You Have a Telegram Bot With 50k Users](https://nerdy.pro/blog/telegram-bot-to-app-6-weeks). If you're trying to figure out what your specific project should cost, we're happy to give you a concrete estimate for free. [Contact us](https://nerdy.pro/contact) with a project brief and we'll come back with a scoped proposal — line items, timelines, and what we're not going to do. --- *Nerdy Production is a Flutter development agency shipping apps across fintech, healthcare, and retail. Our [open-source Flutter packages](https://nerdy.pro/open-source) are used by developers worldwide.* ## How to Generate PDFs in Rust with Skia: A Complete Guide Using the skia-safe Crate https://nerdy.pro/blog/skia-rust-pdf-rendering Generating PDFs programmatically is one of those tasks that sounds simple until you actually try it. Most libraries either give you a low-level stream of bytes to wrestle with, or a high-level template engine that falls apart the moment you need precise control over layout, typography, or graphics. There is a middle ground — and it is the same graphics engine that renders every web page you print to PDF in Chrome, every screen on Android, and every frame in a Flutter app: **Google Skia**. This guide shows how to use Skia from Rust through the `skia-safe` crate to produce vector PDFs with full control over text, shapes, images, and multi-page layouts. ### Key takeaways - **Skia** is Google's open-source 2D graphics library, in active development since 2005, with over 2 million downloads of its Rust bindings on [crates.io](https://crates.io/crates/skia-safe). - The **skia-safe** crate (v0.93) provides safe, idiomatic Rust bindings with a typestate pattern that catches malformed PDF construction at compile time. - Skia produces **true vector PDFs** with selectable text, not rasterized images — the same approach Chrome uses for its print-to-PDF feature. - It supports **PDF/A archival conformance**, **tagged/accessible PDFs** (PDF/UA), **font subsetting** via HarfBuzz, and **multi-page documents** with mixed page sizes. - Prebuilt binaries ship by default, so the first build takes **under one minute** instead of 20-40 minutes from source. ### What is Skia and why use it for PDF generation in Rust Skia is an open-source 2D graphics library maintained by Google. It has been in active development since 2005 and is one of the most battle-tested rendering engines in existence. When you print a web page to PDF in Google Chrome, Skia's PDF backend is what translates the rendered page into a PDF file. When Android draws its UI, Skia handles the rendering. Flutter uses Skia as its rendering engine for cross-platform apps. What makes Skia compelling for programmatic PDF generation in Rust: - **True vector output.** Text remains selectable, shapes stay sharp at any zoom level, and file sizes stay reasonable — because Skia translates its drawing operations into native PDF primitives rather than rasterizing pixels. - **Full 2D graphics API.** Bezier paths, gradients, affine transforms, clip regions, blend modes, and typographic controls — everything you would expect from a professional graphics library, and it all maps to PDF. - **Tagged and accessible PDFs.** Skia supports PDF structure element trees and node IDs, which means you can produce documents that meet PDF/UA accessibility standards. - **PDF/A archival conformance.** A single metadata flag enables PDF/A-2b output for long-term document preservation — required for legal, medical, and government documents. - **Font subsetting.** When you embed fonts, Skia strips out unused glyphs via HarfBuzz, keeping file sizes compact. ### Setting up the skia-safe crate for Rust PDF generation The `skia-safe` crate provides safe, idiomatic Rust bindings for Skia. It wraps the C++ library through FFI while exposing an API that feels natural in Rust — complete with ownership semantics, lifetime guarantees, and a typestate pattern that catches entire categories of bugs at compile time. Add it to your project: ```toml [dependencies] skia-safe = "0.93" ``` The `pdf` feature is enabled by default, so no additional flags are needed. By default, the crate downloads prebuilt Skia binaries for your platform, which means the first build takes under a minute rather than the 20-40 minutes it would take to compile Skia from source. ### How to generate a PDF file in Rust with Skia The core concept is straightforward: create a document, begin a page, draw on the canvas, end the page, close the document. Here is a minimal example that produces a single-page PDF: ```rust use skia_safe::pdf; fn main() { let mut output = Vec::new(); // Create a document — US Letter size (612 x 792 points) let mut document = pdf::new_document(&mut output, None) .begin_page((612, 792), None); let canvas = document.canvas(); // Draw a filled rectangle let mut paint = skia_safe::Paint::default(); paint.set_color(skia_safe::Color::from_rgb(41, 98, 255)); canvas.draw_rect( skia_safe::Rect::from_xywh(72.0, 72.0, 468.0, 100.0), &paint, ); // Draw text let font = skia_safe::Font::default(); paint.set_color(skia_safe::Color::WHITE); canvas.draw_str("Hello from Skia + Rust", (90.0, 130.0), &font, &paint); // Finalize document.end_page().close(); std::fs::write("hello.pdf", &output).unwrap(); } ``` Run this, open `hello.pdf`, and you will see a blue rectangle with white text — all as vector graphics, fully selectable and zoomable. ![The generated PDF opened in a viewer — a blue rectangle with white text rendered as vector graphics on a US Letter page using skia-safe in Rust](https://nerdy.pro/blog/skia-rust-pdf-rendering.webp) ### Compile-time safety with the typestate pattern One of the most elegant aspects of the Rust bindings is the typestate pattern on the `Document` type. The document transitions between two states: `Open` (ready to begin a page) and `OnPage` (actively drawing). ```rust // Document — can begin a page or close the document let document = pdf::new_document(&mut output, None); // Document — can access the canvas or end the page let page = document.begin_page((612, 792), None); let canvas = page.canvas(); // Back to Document — can begin another page or close let document = page.end_page(); document.close(); ``` If you try to call `canvas()` on a document that has no open page, or try to `close()` a document while a page is active, the code will not compile. This is not a runtime check or an assertion — it is a compile-time guarantee enforced by Rust's type system. You cannot produce a malformed PDF because the compiler prevents it. Compare this with C++ or Python PDF libraries where these mistakes result in corrupted output, silent failures, or runtime crashes. In Rust with `skia-safe`, the compiler is your proofreader. ### Adding PDF metadata for SEO and accessibility PDF metadata is what search engines, document management systems, and accessibility tools use to understand your document. Skia exposes this through the `Metadata` struct: ```rust use skia_safe::pdf::{self, Metadata}; let metadata = Metadata { title: "Q4 Financial Report".to_string(), author: "Finance Team".to_string(), subject: "Quarterly results and projections".to_string(), creator: "Report Generator v2.1".to_string(), ..Default::default() }; let mut output = Vec::new(); let document = pdf::new_document(&mut output, Some(&metadata)); ``` For archival compliance, enable PDF/A with a single flag: ```rust let metadata = Metadata { pdf_a: true, ..Default::default() }; ``` This produces PDF/A-2b conformant output — suitable for legal documents, medical records, and any content that needs to be readable decades from now. ### Creating multi-page PDF documents in Rust Real documents are rarely a single page. Skia handles multi-page PDFs naturally — each call to `begin_page` starts a new page, and pages can have different sizes: ```rust let mut output = Vec::new(); let document = pdf::new_document(&mut output, None); // Page 1 — A4 portrait let mut page = document.begin_page((595, 842), None); draw_cover_page(page.canvas()); let document = page.end_page(); // Page 2 — A4 landscape let mut page = document.begin_page((842, 595), None); draw_data_table(page.canvas()); let document = page.end_page(); // Page 3 — back to portrait let mut page = document.begin_page((595, 842), None); draw_charts(page.canvas()); let document = page.end_page(); document.close(); ``` Mixed orientations and page sizes within a single document — something many PDF libraries struggle with — works out of the box. ### Drawing shapes, text, and paths in Skia PDFs Skia's `Canvas` is a full-featured 2D drawing surface. Everything you draw on it is converted to vector PDF primitives. #### Shapes and paths ```rust let canvas = page.canvas(); let mut paint = Paint::default(); paint.set_anti_alias(true); // Filled rectangle paint.set_color(Color::from_rgb(59, 130, 246)); paint.set_style(skia_safe::PaintStyle::Fill); canvas.draw_rect(Rect::from_xywh(50.0, 50.0, 200.0, 100.0), &paint); // Stroked circle paint.set_style(skia_safe::PaintStyle::Stroke); paint.set_stroke_width(2.0); canvas.draw_circle((300.0, 100.0), 50.0, &paint); // Custom bezier path let mut path = skia_safe::Path::new(); path.move_to((400.0, 50.0)); path.cubic_to((450.0, 20.0), (500.0, 80.0), (550.0, 50.0)); path.line_to((550.0, 150.0)); path.close(); paint.set_style(skia_safe::PaintStyle::Fill); canvas.draw_path(&path, &paint); ``` #### Text rendering with selectable output ```rust let typeface = skia_safe::Typeface::from_name("Helvetica", skia_safe::FontStyle::bold()) .unwrap_or_else(skia_safe::Typeface::default); let font = skia_safe::Font::from_typeface(&typeface, 24.0); let mut paint = Paint::default(); paint.set_color(Color::BLACK); canvas.draw_str("Section Title", (72.0, 72.0), &font, &paint); // Smaller body text let body_font = skia_safe::Font::from_typeface(&typeface, 12.0); canvas.draw_str("Body text goes here.", (72.0, 100.0), &body_font, &paint); ``` The text in the output PDF is selectable and searchable — not rasterized images of characters. #### Affine transforms ```rust canvas.save(); canvas.translate((200.0, 300.0)); canvas.rotate(45.0, None); canvas.draw_rect(Rect::from_xywh(-50.0, -25.0, 100.0, 50.0), &paint); canvas.restore(); ``` Transforms are composable and translate directly to PDF's transformation matrix — no rasterization involved. ### Embedding images in Rust-generated PDFs Embedding images in PDFs is where many libraries add unnecessary file size. Skia handles this intelligently: ```rust let image_data = std::fs::read("photo.jpg").unwrap(); let data = skia_safe::Data::new_copy(&image_data); let image = skia_safe::Image::from_encoded(data).expect("Failed to decode image"); canvas.draw_image(&image, (72.0, 200.0), None); ``` When JPEG encoding support is configured in the metadata, Skia passes JPEG data through directly into the PDF without re-encoding — preserving quality and keeping file sizes minimal. Without it, images are deflate-compressed, which can increase file size significantly for photographic content. You can control this through the metadata: ```rust let metadata = Metadata { encoding_quality: 85, // JPEG quality (101 = lossless/deflate) ..Default::default() }; ``` ### Rust PDF library comparison: skia-safe vs printpdf vs krilla The Rust ecosystem offers several approaches to PDF generation. Here is how they compare: | Library | Type | Vector output | Font subsetting | PDF/A | Tagged PDF | Layout engine | Maturity | | --- | --- | --- | --- | --- | --- | --- | --- | | **skia-safe** | Skia bindings (C++ FFI) | Yes | Yes (HarfBuzz) | Yes | Yes | No (canvas API) | 20+ years (Skia) | | **printpdf** | Pure Rust | Yes | Limited | No | No | No | Stable | | **genpdf** | Pure Rust (on printpdf) | Yes | Limited | No | No | Basic | Unmaintained | | **krilla** | Pure Rust (on pdf-writer) | Yes | Yes (OpenType) | Yes | Yes | No | Newer | | **Headless Chrome** | Browser runtime | Yes | Yes | No | No | Full HTML/CSS | Mature | **skia-safe** gives you a full 2D graphics API backed by over 20 years of development at Google. Vector output, font subsetting, tagged PDFs, PDF/A support. The tradeoff is a C++ dependency — though prebuilt binaries make this painless in practice. **printpdf** is pure Rust with no C dependencies. It works well for simple documents but requires manual positioning of every element. No layout engine, limited typography support. **genpdf** builds a higher-level layout API on top of printpdf. It is pure Rust and easier to use for simple documents, but the project has been unmaintained for several years. **krilla** is a newer pure Rust library with a modern API, excellent OpenType support, and features like gradients and blend modes. It is a strong choice if avoiding C dependencies is a priority, though it is less battle-tested than Skia. **Headless Chrome / Puppeteer** gives you full HTML/CSS rendering but requires shipping a browser runtime. Heavyweight, but useful when you need to render complex web layouts. The choice depends on your constraints. If you need precise control over graphics and typography, want proven reliability, and are comfortable with a C++ dependency, Skia is the most capable option. If you need a pure Rust solution, krilla or printpdf are worth evaluating. ### Known limitations of Skia's PDF backend Skia's PDF backend is comprehensive, but it has boundaries worth knowing about: **Blend mode support.** Most PDF-standard blend modes work (Multiply, Screen, Overlay, etc.), but some Skia-specific modes like `SrcATop`, `DstATop`, `Xor`, and `Plus` have no PDF equivalent. If your drawing uses these, the output may differ from what you see on screen. **Rasterization fallback.** Features without a native PDF representation — such as perspective transforms on text — are rasterized at the DPI specified in metadata (`raster_dpi`, default 72). For print-quality output, increase this to 300 or higher, with the tradeoff of larger file sizes. **Build from source.** If you need to customize Skia features beyond what prebuilt binaries offer, building from source takes 20-40 minutes. The `binary-cache` feature (enabled by default) avoids this for standard configurations. ### Production architecture for Rust PDF generation For production systems that generate many documents — invoices, reports, certificates — a clean architecture separates content from rendering: ```rust struct DocumentContent { title: String, sections: Vec
, footer: String, } struct Section { heading: String, body: String, charts: Vec, } fn render_document(content: &DocumentContent) -> Vec { let mut output = Vec::new(); let metadata = Metadata { title: content.title.clone(), creator: "My Document Engine".to_string(), ..Default::default() }; let mut doc = pdf::new_document(&mut output, Some(&metadata)); for section in &content.sections { let mut page = doc.begin_page((595, 842), None); // A4 render_section(page.canvas(), section); doc = page.end_page(); } doc.close(); output } ``` This separation means your business logic never touches the PDF API directly. Content flows in, bytes flow out. You can test the rendering independently, swap out the PDF backend without changing business logic, and parallelize document generation across threads — since each `Document` owns its output buffer. ### Getting started with Skia PDF generation in Rust Add `skia-safe` to your `Cargo.toml`, write a few lines of drawing code, and open the resulting PDF. The learning curve is gentle if you have used any 2D graphics API before — Canvas, Cairo, Core Graphics, or even HTML5 Canvas. The concepts transfer directly. The `skia-safe` crate is at version 0.93 with over [2 million downloads on crates.io](https://crates.io/crates/skia-safe). The prebuilt binary cache means your first build is fast, and the typestate pattern means your first PDF is correct. For applications that need reliable, high-quality PDF output — whether that is invoices, reports, tickets, certificates, or any document where layout precision matters — Skia through Rust gives you the same rendering engine trusted by billions of devices, with the safety guarantees that Rust is known for. We used this exact approach to build [dxpdf](https://nerdy.pro/open-source/dxpdf) — our open-source DOCX-to-PDF converter written in Rust and powered by Skia. It converts Word documents to high-fidelity PDFs in ~115ms without requiring Microsoft Office or LibreOffice. ### Frequently asked questions #### What is the best Rust library for generating PDFs? It depends on your requirements. **skia-safe** is the most feature-complete option, offering vector output, font subsetting, tagged PDFs, and PDF/A support through Google's Skia engine. For pure Rust without C dependencies, **krilla** and **printpdf** are strong alternatives. For rendering HTML/CSS to PDF, headless Chrome remains the most reliable approach. #### Does Skia produce vector or rasterized PDFs? Skia produces **true vector PDFs**. Text is selectable and searchable, shapes remain sharp at any zoom level, and drawing operations are translated into native PDF primitives. Rasterization only occurs as a fallback for features that have no PDF-native representation, such as perspective transforms. #### Can I create accessible and PDF/A-compliant documents with skia-safe? Yes. Skia supports PDF structure element trees and node IDs for creating tagged, accessible PDFs that meet PDF/UA standards. For archival compliance, setting `pdf_a: true` in the `Metadata` struct produces PDF/A-2b conformant output. #### How long does skia-safe take to build? With the default `binary-cache` feature enabled, the first build takes **under one minute** because prebuilt Skia binaries are downloaded for your platform. Without the cache (building Skia from C++ source), expect 20-40 minutes. #### Is skia-safe the same engine that Chrome uses for print-to-PDF? Yes. When you use Chrome's "Save as PDF" or "Print to PDF" feature, the browser renders the page through Skia's PDF backend — the same engine that `skia-safe` exposes to Rust. This is also the rendering engine used by Android for its UI and by Flutter for cross-platform apps. #### How does skia-safe handle fonts in PDFs? Skia embeds fonts directly into the PDF. With HarfBuzz integration (enabled via the `textlayout` feature), it performs font subsetting — stripping out unused glyphs to keep file sizes compact. TrueType fonts use glyph-ID encoding for accurate rendering. ## Encryption Explained: From AES to Post-Quantum Cryptography and End-to-End Messaging https://nerdy.pro/blog/encryption-explained Every message you send, every payment you make, and every password you type relies on encryption. It is the invisible layer that keeps digital life private — and it is built into virtually every application your business depends on. This article explains how different types of encryption protect real-world applications, why emerging technologies like elliptic curves and post-quantum algorithms matter for your products, and what Instagram's recent decision to drop end-to-end encryption tells us about the tension between privacy and regulation. ### What encryption actually does Encryption transforms readable data (plaintext) into an unreadable form (ciphertext) using a secret value called a key. Only someone who possesses the correct key can reverse the process and recover the original data. Two properties matter most: - **Confidentiality** — an attacker who intercepts the ciphertext learns nothing about the plaintext. - **Integrity** — modern encryption schemes also detect if the ciphertext was tampered with in transit. All practical encryption falls into one of two families: symmetric and asymmetric. Real-world systems use them together, and understanding why helps you make informed decisions about the applications you build or use. ### Symmetric encryption: the workhorse of data protection In symmetric encryption, the same key is used to encrypt and decrypt. Think of it as a safe with a single combination: anyone who knows the combination can open it, and keeping that combination secret is what keeps the contents secure. #### Where you encounter it every day **AES (Advanced Encryption Standard)** is the most widely used symmetric cipher in the world. Adopted by NIST in 2001, it supports key lengths of 128, 192, or 256 bits and is backed by hardware acceleration in virtually every modern processor. Here is where AES protects your data in practice: - **Cloud storage.** When you upload files to AWS S3, Google Cloud Storage, or Azure Blob Storage, they are encrypted at rest using AES-256. Even if someone gains physical access to the storage hardware, the data is unreadable without the key. - **Database encryption.** PostgreSQL, MongoDB, and MySQL all offer AES-based encryption to protect data on disk. This means a stolen database backup is useless without the encryption keys. - **HTTPS / TLS.** Every time your browser shows a padlock icon, AES is doing the heavy lifting. TLS 1.3 uses AES-GCM (Galois/Counter Mode) as its primary cipher — it encrypts data and verifies its integrity simultaneously, protecting everything from login credentials to API calls. - **Mobile applications.** Both Android's file-based encryption and Apple's Data Protection use AES-256 to encrypt data on the device. When a user locks their phone, the encryption keys are evicted from memory, making the stored data inaccessible. - **Payment processing.** PCI DSS — the security standard for handling credit card data — requires AES encryption for cardholder data both in transit and at rest. **ChaCha20-Poly1305** is another symmetric cipher gaining adoption. It is faster than AES on devices without hardware AES acceleration — particularly older smartphones and IoT devices. Google chose it for TLS connections on Android, and it is the default cipher in WireGuard VPN. #### Try it yourself: AES encryption in your terminal You do not need any special software — OpenSSL is pre-installed on macOS, Linux, and most server environments. Open a terminal and try these commands. Encrypt a message with AES-256: ```bash echo "I want to secure my app with AES" | openssl enc -aes-256-cbc -a -salt -pass pass:nerdy_pro ``` The output will be a block of random-looking Base64 text — your encrypted ciphertext. To decrypt it, take that output and run: ```bash echo "U2FsdGVkX181dnX8SWIhZTumWK0Uc7Kh947omVSNpH8lujB43L5OjlubfkGa3Y1dyxj5cfzyPZjhWiyGzIWdPw==" | openssl enc -aes-256-cbc -a -d -salt -pass pass:nerdy_pro ``` You will get your original message back. OpenSSL may show a warning: `*** WARNING : deprecated key derivation used.` — this is expected. It means OpenSSL defaults to an older key derivation method when using `-pass pass:`. In production, you would use `-pbkdf2` for stronger key derivation, but for this demonstration the default works fine. Now try changing a single character in the password and decrypting again — it will fail. This demonstrates the core property of symmetric encryption: without the exact key, the data is unrecoverable. You can also encrypt entire files: ```bash # Encrypt openssl enc -aes-256-cbc -salt -in report.pdf -out report.pdf.enc -pass pass:nerdy_pro # Decrypt openssl enc -aes-256-cbc -d -in report.pdf.enc -out report-decrypted.pdf -pass pass:nerdy_pro ``` #### The key distribution problem Symmetric encryption is fast and efficient, but both parties must share the same secret key before they can communicate. If your application serves a million users, you cannot pre-share a million different keys securely. This is the problem that asymmetric cryptography solves. ### Asymmetric encryption: enabling trust between strangers Asymmetric encryption uses a pair of keys: a **public key** that can be shared openly, and a **private key** that must remain secret. Data encrypted with the public key can only be decrypted with the corresponding private key. #### RSA: the foundation of internet security RSA, published in 1977, was the first practical public-key cryptosystem. Its security rests on the difficulty of factoring the product of two very large prime numbers — a problem that is easy to set up but virtually impossible to reverse with current technology. Here is how RSA powers real applications: - **SSL/TLS certificates.** When you visit a website over HTTPS, the server presents a certificate signed with RSA (or ECC). Your browser verifies this signature to confirm you are talking to the real server and not an imposter. Certificate authorities like Let's Encrypt issue millions of RSA-signed certificates. - **Code signing.** When you download an app from the Apple App Store or install a Windows application with a digital signature, RSA signatures verify that the code has not been tampered with since the developer published it. - **Email encryption.** PGP/GPG and S/MIME use RSA to encrypt emails and sign them, ensuring both privacy and sender authenticity. - **SSH access.** System administrators use RSA key pairs to securely access servers without passwords — `ssh-keygen -t rsa` is one of the most common commands in any DevOps workflow. #### Try it yourself: RSA encryption in your terminal Generate an RSA key pair: ```bash # Generate a 2048-bit private key openssl genrsa -out private.pem 2048 # Extract the public key openssl rsa -in private.pem -pubout -out public.pem ``` Now encrypt a message with the public key and decrypt it with the private key: ```bash # Encrypt — anyone with your public key can do this echo "I want to secure my app with RSA" | openssl pkeyutl -encrypt -pubin -inkey public.pem -out message.enc # Decrypt — only the private key holder can do this openssl pkeyutl -decrypt -inkey private.pem -in message.enc ``` This is the core idea: your business partner encrypts data with your public key, sends the ciphertext over any channel (even email), and only you can read it. Without the private key, the encrypted file is useless. #### The hybrid approach In practice, asymmetric encryption is too slow for encrypting large amounts of data. Instead, real systems use a **hybrid approach**: RSA (or ECC) securely exchanges a one-time symmetric key, and then AES handles the bulk data encryption at high speed. This is exactly how TLS, SSH, and PGP work under the hood. ### Elliptic curve cryptography: stronger security, smaller keys Elliptic Curve Cryptography (ECC) achieves the same security level as RSA with dramatically smaller keys. This translates directly into faster connections, smaller certificates, lower bandwidth, and better performance on mobile and IoT devices. #### The practical difference | Security level | RSA key size | ECC key size | | --- | --- | --- | | Standard (128-bit) | 3072 bits | 256 bits | | High (192-bit) | 7680 bits | 384 bits | | Ultra (256-bit) | 15360 bits | 521 bits | A 256-bit ECC key provides the same protection as a 3072-bit RSA key — that is a **12x reduction** in key size. For applications that handle thousands of connections per second or run on battery-powered devices, this difference is significant. #### Where ECC is used today - **TLS 1.3.** The latest version of TLS exclusively uses ECC-based key exchange (ECDHE). RSA key exchange has been removed entirely. Every modern HTTPS connection you make uses elliptic curves. - **Signal and WhatsApp.** The Signal Protocol uses Curve25519 — a specific elliptic curve designed for high performance and resistance to implementation errors — for its key exchange. This is the same protocol that protects messages for over two billion WhatsApp users. - **WireGuard VPN.** WireGuard uses Curve25519 for all key exchanges, contributing to its reputation as a simpler and faster VPN compared to OpenVPN or IPsec. - **SSH keys.** Ed25519 — an elliptic curve signature scheme — has become the recommended key type for SSH. It produces shorter keys and faster signatures than RSA: `ssh-keygen -t ed25519` generates a key that is both more secure and more convenient than the older RSA default. - **Cryptocurrency.** Bitcoin and Ethereum use the secp256k1 elliptic curve to sign every transaction. When you send cryptocurrency, an ECC signature proves you own the wallet without revealing your private key. - **Apple and Google sign-in.** Sign in with Apple uses ECC (P-256) for its identity tokens. Google's Cloud KMS supports ECC keys for signing and verification. #### Try it yourself: ECC keys in your terminal Generate an elliptic curve key pair and compare it with RSA: ```bash # Generate an ECC private key (Curve P-256) openssl ecparam -genkey -name prime256v1 -noout -out ec-private.pem # Extract the public key openssl ec -in ec-private.pem -pubout -out ec-public.pem # Compare file sizes — ECC keys are dramatically smaller wc -c private.pem ec-private.pem ``` You will see the ECC private key is roughly 4-5x smaller than the RSA one, while providing equivalent security. On a server handling thousands of TLS handshakes per second, this difference adds up fast. ### Digital signatures and checksums: proving authenticity and integrity Encryption keeps data secret, but two equally important questions remain: **who sent this data?** and **was it modified in transit?** Digital signatures and checksums answer these questions — and they are just as fundamental to application security as encryption itself. #### Checksums and CRCs: detecting accidental corruption A checksum is a short value computed from a block of data. If even one bit of the data changes, the checksum changes too. The simplest form is the **CRC (Cyclic Redundancy Check)** — a fast algorithm designed to catch accidental errors during data transfer or storage. You use CRCs more often than you might think: - **File downloads.** When you download a Linux ISO or a software package, the website often lists an SHA-256 or MD5 checksum. You compute the checksum of your downloaded file and compare it — if they match, the file arrived intact. - **Network protocols.** Ethernet frames, TCP packets, and ZIP files all include CRC checksums to detect corruption during transmission. - **Database replication.** Systems like PostgreSQL use checksums to detect data corruption on disk before it propagates to replicas. - **Git.** Every commit, file, and tree in a Git repository is identified by a SHA-1 hash. This is why Git can detect any modification to the repository history. Try it in your terminal: ```bash # Create a file and compute its SHA-256 checksum echo "I want to secure my app with checksums" > document.txt shasum -a 256 document.txt # Modify even one character and the checksum changes completely echo "I want to secure my app with Checksums" > document.txt shasum -a 256 document.txt ``` The two checksums will be entirely different — not just slightly different, but completely unrecognizable from each other. This property is called the **avalanche effect**, and it is what makes checksums reliable for detecting changes. **Important distinction:** CRCs and simple checksums detect *accidental* corruption — they do not protect against *intentional* tampering. An attacker who modifies a file can simply recompute the checksum. To defend against deliberate modification, you need cryptographic signatures. #### Digital signatures: proving who sent the data A digital signature uses asymmetric cryptography in reverse: instead of encrypting with the public key and decrypting with the private key, the sender **signs** a message with their private key, and anyone with the public key can **verify** the signature. This gives you two guarantees: - **Authentication** — the message was created by the holder of the private key and nobody else. - **Integrity** — the message was not altered after signing. Digital signatures are everywhere in production systems: - **Software updates.** When your phone installs an iOS or Android update, the OS verifies Apple's or Google's digital signature before applying it. A modified update without a valid signature is rejected — this is the primary defense against malicious firmware. - **API authentication.** JWTs (JSON Web Tokens) used for API auth are digitally signed. When your backend receives a JWT, it verifies the signature to confirm the token was issued by your auth server and has not been tampered with. - **Package managers.** npm, PyPI, Docker Hub, and APT all use signatures to verify that packages were published by their claimed authors. Supply chain attacks — like the infamous event-stream incident — exploit gaps in this verification. - **Blockchain transactions.** Every Bitcoin or Ethereum transaction is signed with the sender's private key. The network verifies the signature before accepting the transaction — this is how cryptocurrency works without a central authority. - **Document signing.** PDF documents, contracts, and invoices can carry digital signatures that prove authorship and detect modifications — recognized as legally binding in most jurisdictions under eIDAS (EU) and ESIGN (US) regulations. Try signing and verifying a message yourself: ```bash # Create a document echo "I want to secure my app with digital signatures" > message.txt # Sign it with your RSA private key (from the earlier example) openssl dgst -sha256 -sign private.pem -out message.sig message.txt # Verify the signature with the public key openssl dgst -sha256 -verify public.pem -signature message.sig message.txt # Output: Verified OK # Now tamper with the file and verify again echo "I want to secure my app with Digital Signatures" > message.txt openssl dgst -sha256 -verify public.pem -signature message.sig message.txt # Output: Verification Failure ``` The tampered message fails verification — the signature proves both who created the document and that it has not been modified. This is the same mechanism that protects every HTTPS certificate, every signed Git commit, and every app you install from an app store. #### HMAC: signatures for symmetric systems When both parties share a secret key (as in many API integrations), **HMAC (Hash-based Message Authentication Code)** provides integrity and authentication without the overhead of public-key cryptography. Stripe, AWS, and GitHub all use HMAC to sign webhook payloads so your server can verify they are genuine: ```bash # Compute an HMAC-SHA256 of a message echo -n "I want to secure my app with HMAC" | openssl dgst -sha256 -hmac "nerdy_pro" ``` Your server computes the same HMAC using its copy of the shared secret and compares it to the signature in the request header. If they match, the webhook is authentic. #### JWTs: signatures in practice JSON Web Tokens (JWTs) are one of the most common real-world applications of digital signatures. If your application has user authentication, there is a good chance JWTs are involved. A JWT is a compact, URL-safe token with three parts separated by dots: a header, a payload, and a signature. ```text eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0Miwicm9sZSI6Im5lcmR5X2FkbWluIn0.oAi5ALawUM_KpWDkiJcfA8504oQ_Yjx7OhEM8nZxlIc ``` The header specifies the signing algorithm. The payload carries the actual data — user ID, role, expiration time. The signature ensures neither has been tampered with. Your server verifies the signature on every request and trusts the payload only if verification passes. Try decoding this token yourself — paste it into [jwt.io](https://jwt.io/) and see what is inside. You will notice you can read the payload without knowing the secret. That is by design: JWTs are signed, not encrypted. The signature only proves the token has not been tampered with — it does not hide the contents. **Signing algorithms** determine the security model: - **HS256 (HMAC-SHA256)** — symmetric. The same secret key signs and verifies the token. Simple to set up, but every service that needs to verify tokens must have the secret — which becomes a risk as your architecture grows. - **RS256 (RSA-SHA256)** — asymmetric. The auth server signs with a private key, and any service verifies with the public key. The public key can be distributed freely without compromising security. - **ES256 (ECDSA-P256)** — asymmetric, using elliptic curves. Same trust model as RS256, but with smaller keys and faster signatures. Increasingly the recommended choice for new applications. **Advantages of JWTs:** - **Stateless authentication.** The server does not need to query a database or session store on every request — the token itself contains all the information needed to authorize the user. This simplifies scaling horizontally across multiple servers. - **Cross-service trust.** In microservice architectures, the auth service signs the token once, and every downstream service can verify it independently using the public key. No shared database or network call required. - **Standard and portable.** JWTs are an open standard (RFC 7519) supported by libraries in every major programming language. Tokens can be passed in HTTP headers, URL parameters, or cookies. **Pitfalls to watch for:** - **Tokens cannot be revoked.** Once a JWT is signed, it is valid until it expires. If a user's account is compromised, you cannot invalidate their existing tokens without adding a server-side blocklist — which partially defeats the stateless benefit. Short expiration times (15 minutes) combined with refresh tokens mitigate this. - **Payload is not encrypted.** The Base64-encoded payload is signed but not encrypted — anyone who intercepts the token can read its contents. Never put sensitive data (passwords, personal information, API keys) in a JWT payload. If you need an encrypted token, use JWE (JSON Web Encryption), though this adds complexity. - **Algorithm confusion attacks.** If the server accepts multiple algorithms and does not strictly validate the `alg` header, an attacker can forge tokens — for example, by switching from RS256 to HS256 and signing with the public key as the HMAC secret. Always enforce the expected algorithm on the server side. - **Oversized tokens.** Developers sometimes put too much data in the payload — entire user profiles, permission lists, session state. JWTs are sent with every request, usually in an HTTP header. Large tokens increase bandwidth, may exceed header size limits, and slow down every API call. Keep payloads minimal: user ID, role, expiration. For most applications, JWTs with ES256 signing and short expiration times provide a good balance of security, performance, and simplicity. If you need token revocation, pair them with a lightweight server-side check against a revocation list or use opaque refresh tokens that can be invalidated independently. ### The quantum threat: a real risk that demands preparation now Quantum computers use fundamentally different physics — qubits that can exist in multiple states simultaneously — to solve certain problems exponentially faster than any classical computer. For encryption, this has specific and well-understood consequences. #### What quantum computers will break In 1994, Peter Shor published [a quantum algorithm](https://ieeexplore.ieee.org/document/365700) proving that a sufficiently powerful quantum computer could break RSA, ECC, and Diffie-Hellman — essentially every asymmetric algorithm in use today. This is not theoretical speculation: it is a proven mathematical result. The only open question is when quantum hardware will be powerful enough to execute it at scale. Symmetric ciphers like AES are affected less severely. [Grover's algorithm](https://arxiv.org/abs/quant-ph/9605043) effectively halves the security of symmetric keys — making AES-128 inadequate but leaving AES-256 with a comfortable 128-bit security margin. #### "Harvest now, decrypt later" This is the threat that makes quantum computing an urgent concern today, not in some distant future. Intelligence agencies and sophisticated attackers are already intercepting and storing encrypted traffic, planning to decrypt it once quantum hardware matures. This strategy is well-documented in [NSA's post-quantum cybersecurity guidance](https://www.nsa.gov/Cybersecurity/Post-Quantum-Cybersecurity-Resources/). For any application that handles data with a long confidentiality lifespan — medical records, financial data, legal communications, intellectual property — this is a present risk, not a hypothetical one. #### Post-quantum cryptography: the industry response In 2024, [NIST finalized its first post-quantum cryptographic standards](https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards): - **ML-KEM (CRYSTALS-Kyber)** — a key exchange mechanism that replaces ECDH. Already deployed in production: Chrome and Cloudflare use a hybrid X25519 + ML-KEM key exchange by default. - **ML-DSA (CRYSTALS-Dilithium)** — a digital signature scheme that replaces RSA and ECDSA signatures. - **SLH-DSA (SPHINCS+)** — a backup signature scheme based on hash functions, providing algorithmic diversity in case lattice-based schemes are found vulnerable. #### What this means for your applications If you are building or maintaining applications today, here is what matters: 1. **Upgrade to AES-256.** This is the simplest step and provides quantum-resistant symmetric encryption at virtually no performance cost. 2. **Enable hybrid key exchange.** Modern TLS libraries (OpenSSL 3.x, BoringSSL) already support X25519 + ML-KEM. Enabling it protects connections against both classical and quantum attacks. 3. **Audit your cryptographic dependencies.** Know where RSA and ECC are used in your stack — these are the components that will eventually need migration. If your application was built quickly or with AI coding tools, an [AI-generated code audit](https://nerdy.pro/services/ai-code-audit) is the fastest way to find where secrets and weak crypto are hiding. 4. **Plan for larger keys and signatures.** Post-quantum algorithms produce larger keys than ECC. Test that your protocols, certificates, and storage can accommodate them. ### End-to-end encryption: the gold standard for private messaging End-to-end encryption (E2EE) means that messages are encrypted on the sender's device and can only be decrypted on the recipient's device. The service provider — whether it is a messaging app, email platform, or cloud storage — cannot read the content, even if compelled by a court order or compromised by an attacker. #### How modern E2EE works in practice The Signal Protocol, used by Signal and WhatsApp, combines multiple encryption techniques into a layered system: 1. **Elliptic curve key exchange (Curve25519)** — two devices establish a shared secret without ever transmitting it over the network. 2. **The Double Ratchet algorithm** — generates a new encryption key for every single message. If an attacker somehow obtains one key, they cannot decrypt past or future messages. This property is called forward secrecy. 3. **AES-256 or ChaCha20** — encrypts the actual message content at high speed. 4. **HMAC authentication** — ensures messages cannot be tampered with in transit. The result is that every message has a unique key, keys are never reused, and compromise of any single key is contained. This is why security researchers consistently [recommend the Signal Protocol](https://eprint.iacr.org/2016/1013.pdf) as the state of the art in messaging security. #### The group messaging challenge E2EE in one-on-one conversations is a solved problem. Group chats are significantly harder. When a group has 50, 200, or 1000 members, the protocol must handle several additional complexities: - **Key distribution at scale.** Every message must be encrypted so that all current group members — and only current members — can read it. The Signal Protocol uses a mechanism called Sender Keys: each participant shares a symmetric key with the group, and messages are encrypted once rather than once per recipient. This keeps group messaging performant even at large sizes. - **Member changes.** When someone joins or leaves a group, the encryption keys must be rotated so that the new member cannot read past messages and the departed member cannot read future ones. WhatsApp and Signal handle this automatically, but it adds latency and overhead — particularly in large groups with frequent membership changes. - **Multi-device support.** If a user has a phone, a tablet, and a desktop client, each device has its own encryption keys. The protocol must ensure all devices can decrypt group messages while keeping forward secrecy intact. Apple's iMessage protocol and Signal's multi-device architecture take different approaches to this trade-off. For businesses building group collaboration features — team chats, project channels, shared workspaces — these are not theoretical concerns. The architecture choices made early in development determine whether E2EE is feasible at the scale the product will eventually need. #### Instagram's E2EE reversal: a cautionary tale In December 2023, Meta rolled out default E2EE for Messenger and began extending it to Instagram Direct Messages. The engineering effort was massive — Meta's team described the [multi-year infrastructure overhaul](https://engineering.fb.com/2024/02/22/security/end-to-end-encryption-messenger-update/) required to rebuild features like search, link previews, and spam detection without server-side access to message content. Then, in March 2026, Meta reversed course. The company announced that **Instagram will remove E2EE from Direct Messages on May 8, 2026**. Unlike WhatsApp, where E2EE is the default for all users, Instagram's encrypted messaging was only available as an opt-in feature in select regions — and Meta cited low adoption as the reason for shutting it down. This reversal is significant for several reasons: **Privacy as a feature, not a guarantee.** Instagram's E2EE was never truly "default" — users had to opt in on a per-chat basis, and it was only available in certain countries. This stands in stark contrast to WhatsApp, where every message has been end-to-end encrypted since 2016 with no user action required. The lesson: if encryption is not on by default, adoption will remain low, and that low adoption becomes the justification for removing it. **Regulatory pressure wins.** Governments in the US, UK, EU, and Australia have pushed back hard against E2EE in messaging, arguing it hampers investigations into child exploitation and terrorism. The UK's Online Safety Act includes provisions that could require platforms to break E2EE. The EU's proposed "Chat Control" regulation would mandate client-side scanning of messages before encryption. Removing E2EE from Instagram DMs allows Meta to scan messages for child sexual abuse material (CSAM) and other harmful content — aligning with regulatory expectations. **Users lose control.** Meta told affected users to download their encrypted messages and media before the May 8 deadline, as encrypted chat history will not be migrated to standard unencrypted chats. Users who want E2EE are directed to WhatsApp instead. **The E2EE landscape today.** Here is how major platforms compare after Instagram's reversal: | Platform | E2EE by default | Protocol | | --- | --- | --- | | Signal | Yes (always) | Signal Protocol | | WhatsApp | Yes (since 2016) | Signal Protocol | | iMessage | Yes | Apple's custom protocol | | Instagram DM | **Removed** (May 2026) | N/A | | Telegram | **No** (opt-in "Secret Chats" only) | MTProto | | Discord | No | N/A (encrypted in transit only) | The gap between platforms that treat encryption as a core architectural commitment (Signal, WhatsApp, iMessage) and those that treat it as an optional feature (Instagram, Telegram, Discord) has never been clearer. For businesses building messaging features, this is the critical design decision: E2EE must be foundational, not bolted on — because optional encryption can be taken away. #### What E2EE does not protect For any application that implements or relies on E2EE, it is important to understand the limits: - **Metadata.** E2EE protects content, not metadata. The platform still knows who messaged whom, when, how often, and from which IP address. [Research by EFF](https://www.eff.org/deeplinks/2013/06/why-metadata-matters) has shown that metadata alone can reveal sensitive information about relationships and behavior patterns. - **Endpoint compromise.** If the device itself is compromised — by malware, spyware like [Pegasus](https://citizenlab.ca/2021/07/hooking-candiru-another-mercenary-spyware-vendor-comes-into-focus/), or physical access — the attacker reads messages after decryption. E2EE protects data in transit and on the server, not on a compromised device. - **Cloud backups.** If chat backups are stored unencrypted in iCloud or Google Drive, E2EE is effectively bypassed. WhatsApp offers encrypted backups, but users must enable them. - **Human factors.** No cryptographic protocol prevents a recipient from taking a screenshot or being socially engineered into sharing a conversation. ### How it all fits together in a real application When we build secure applications for our clients, encryption is not a single feature — it is a layered architecture where each type of encryption plays a specific role: 1. **Elliptic curve cryptography** establishes trust between parties and securely exchanges keys — with smaller, faster keys than older RSA-based approaches. 2. **Symmetric encryption (AES-256)** handles the high-speed encryption of actual data — files, messages, database records, API payloads. 3. **Digital signatures and checksums** prove authenticity and detect tampering — from signed API tokens and software updates to verified webhook payloads. 4. **End-to-end encryption** ensures that even the service operator cannot access user content — essential for healthcare, legal tech, finance, and any application where user privacy is a regulatory or competitive requirement. 5. **Post-quantum readiness** protects long-lived data against future threats — worth addressing now for any data that must stay confidential for years. The right encryption strategy depends on what you are building: a mobile app handling personal data, a B2B platform processing financial records, a messaging feature, or an IoT system. Each has different threat models and regulatory requirements, and the encryption architecture should match. Getting this architecture right — and catching the places where it silently breaks, like secrets committed to source control or personal data logged in plain text — is exactly what we look for in an [AI-generated code audit](https://nerdy.pro/services/ai-code-audit). If you are shipping an application that handles sensitive data and want a second set of eyes on its security, [get in touch](https://nerdy.pro/contact). ### References - Shor, P. (1994). [Algorithms for quantum computation: discrete logarithms and factoring](https://ieeexplore.ieee.org/document/365700). Proceedings 35th Annual Symposium on Foundations of Computer Science. - Grover, L. (1996). [A fast quantum mechanical algorithm for database search](https://arxiv.org/abs/quant-ph/9605043). arXiv. - Cohn-Gordon, K. et al. (2016). [A formal security analysis of the Signal messaging protocol](https://eprint.iacr.org/2016/1013.pdf). Cryptology ePrint Archive. - NIST (2024). [Post-Quantum Encryption Standards](https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards). - NSA (2022). [Post-Quantum Cybersecurity Resources](https://www.nsa.gov/Cybersecurity/Post-Quantum-Cybersecurity-Resources/). - Meta Engineering (2024). [End-to-end encryption on Messenger](https://engineering.fb.com/2024/02/22/security/end-to-end-encryption-messenger-update/). - EFF (2013). [Why metadata matters](https://www.eff.org/deeplinks/2013/06/why-metadata-matters). - Citizen Lab (2021). [Hooking Candiru: another mercenary spyware vendor comes into focus](https://citizenlab.ca/2021/07/hooking-candiru-another-mercenary-spyware-vendor-comes-into-focus/). ## Why 0.1 + 0.2 ≠ 0.3 and What It Means for Your Money https://nerdy.pro/blog/floating-point-and-money March 14th is an unofficial holiday in the programming world: Pi Day (3.14). It's the perfect occasion to talk about how computers handle fractional numbers, why arithmetic results sometimes look unexpected, and what consequences this can have in real projects — especially when money is involved. ### The experiment that surprises every beginner Open an interactive console in any programming language and evaluate the simplest expression: ```text 0.1 + 0.2 ``` Dart, JavaScript, Python, Go, C++, Ruby — the result is the same: `0.30000000000000004`. Not `0.3`, as common sense would suggest, but a number with a long tail of zeros and a four at the end. This is not a bug in any particular language, nor an error in your code. It is a fundamental property of how modern hardware represents fractional numbers in memory. ### How computers store fractional numbers All information in a computer is stored in binary — sequences of zeros and ones. For integers, this works flawlessly: `5` in binary is `101`, `10` is `1010`. Every integer has an exact binary representation. Fractional numbers are a fundamentally different story. To convert a decimal fraction to binary, we repeatedly multiply by 2 and take the integer part. Let's see what happens with 0.1: ```text 0.1 × 2 = 0.2 → 0 0.2 × 2 = 0.4 → 0 ← cycle starts 0.4 × 2 = 0.8 → 0 0.8 × 2 = 1.6 → 1 0.6 × 2 = 1.2 → 1 ← cycle ends 0.2 × 2 = 0.4 → 0 ← cycle repeats ... ``` The result is `0.0(0011)` — an infinite repeating binary fraction. This is analogous to how `1/3` becomes the infinite `0.3333...` in decimal — it is impossible to write it exactly in a finite number of digits. The number 0.2 faces the same problem. Both numbers involved in our addition already contain a rounding error the moment they are stored in memory. ### The IEEE 754 standard: a trade-off between precision and performance The vast majority of modern processors use the IEEE 754 standard for floating-point arithmetic. The `double` type (64-bit double precision), which is the default in most programming languages, is structured as follows: - **1 bit** for the sign (positive or negative) - **11 bits** for the exponent (the order of magnitude) - **52 bits** for the mantissa (the significant digits) 52 bits of mantissa translates to roughly 15–17 significant decimal digits. When the binary representation of a fraction is infinite, it gets truncated at the 52nd bit. This is the exact moment the representation error is introduced. When you add two numbers that each contain a rounding error, those errors accumulate. This is how `0.1 + 0.2` becomes `0.30000000000000004`. ### When the error is acceptable To be fair, for most tasks this error is completely negligible. We are talking about an error on the order of 10⁻¹⁶ — one quadrillionth. `double` works perfectly well in the following domains: - **Computer graphics and rendering** — a difference in the sixteenth decimal place is invisible to the human eye - **Physics simulations in games** — objects behave realistically, and the error has no perceptible effect - **Scientific computing** — results are typically rounded to the required precision anyway - **Statistics and machine learning** — models operate on approximations by nature ### When the error becomes critical: financial calculations The situation changes drastically when money is involved. In financial systems, every cent must be accounted for exactly. An error in the sixteenth decimal place may seem insignificant for a single transaction, but when processing thousands and millions of operations, these microscopic rounding errors accumulate and turn into real discrepancies in balances. Consider a concrete example. Suppose a system processes 100,000 transactions per day, and each one introduces an error of `0.000000000000004`. Per day, that's `0.0000000004` — a negligible amount. But multiply that by a year, add more complex operations involving multiplication and division where errors grow significantly faster, and you end up with sums that neither an accountant nor an auditor can explain. ### Reliable approaches to handling monetary amounts #### Approach one: minor units as integers The idea is straightforward: instead of storing an amount in dollars or euros as a fractional number, it is stored in the smallest currency units (cents, pennies, kopecks) as an integer. `1999` cents is always exactly $19.99. No rounding, no surprises, no `0.000000004` trailing behind. Integer arithmetic in computers is absolutely precise. This approach is widely used in payment systems. Stripe, for instance, handles all amounts exclusively in the smallest currency units. It is also worth mentioning an alternative for APIs: transmitting monetary amounts as strings. In this case, the interface remains human-readable (`"19.99"` is clearer than `1999`), while the responsibility for parsing and choosing the appropriate numeric type falls on the client side. #### Approach two: arbitrary-precision libraries (BigDecimal) The second reliable option is to use specialized data types that store decimal numbers without converting them to binary: - **Dart** — the `decimal` package - **Java** — `java.math.BigDecimal` - **Python** — the `decimal` module from the standard library - **JavaScript** — the [Decimal](https://github.com/tc39/proposal-decimal) proposal is under consideration; in the meantime, libraries like `decimal.js` - **Go** — the `shopspring/decimal` package These types represent numbers in decimal form, completely avoiding binary approximations. The expression `0.1 + 0.2 == 0.3` when using `BigDecimal` returns `true` — guaranteed. The trade-off for precision is performance: operations with `BigDecimal` are significantly slower than with `double`. However, in the context of financial calculations where correctness matters more than speed, this is a perfectly acceptable compromise. ### Which approach to choose Both approaches solve the problem, but they suit different situations: | Criterion | Minor units (`int`) | BigDecimal | | --- | --- | --- | | Performance | Maximum | Lower | | Ease of implementation | High | Medium (depends on language) | | Fractional unit support | No (integers only) | Yes | | Multi-currency support | Requires knowing decimal places | Works out of the box | For most e-commerce and payment systems, the minor units approach is optimal. `BigDecimal` is preferable in scenarios that require intermediate fractional calculations — for example, computing interest, taxes, or currency conversions. ### Conclusion If your project deals with monetary amounts, stick to one of two rules: - Store and transmit amounts in minor units as integers (`int`) - Use arbitrary-precision types (`BigDecimal` and its equivalents) Using `double` for financial calculations is technical debt that may not manifest for months, but will eventually lead to discrepancies whose root cause will be extremely difficult to diagnose. Choose tools that match the task, and pay attention to how your programming language represents numbers in memory. This is one of those things worth knowing about before it becomes a production incident. We handle exactly these trade-offs when building money-handling apps — our fintech work on [ExtraETF](https://nerdy.pro/portfolio/extraetf) and our broader [Flutter app development](https://nerdy.pro/services/flutter-app-development) practice both lean on these rules. Happy Pi Day! 🥧 ## HTTPS Made Simple: Getting a Free SSL Certificate https://nerdy.pro/blog/https-free-ssl-certificate Why do you need HTTPS? What is a certificate authority? What types of certificates exist and how do you get one? Setting up nginx with Let's Encrypt. TLS is just the baseline. For a full review of your app's security posture, see our [AI code audit](https://nerdy.pro/services/ai-code-audit) service. ## Databases and Indexes Explained Simply https://nerdy.pro/blog/databases-and-indexes How databases work under the hood, why indexes matter, and how it all fits together. Query and database performance is one of the areas we dig into during an [AI code audit](https://nerdy.pro/services/ai-code-audit). ## Glossary - [AOSP](https://nerdy.pro/glossary#aosp): The Android Open Source Project — Android without the Google layer on top, which anyone may fork. Point-of-sale terminals, kiosks and in-car systems run on forks of it, and working at that level exposes parts of the OS an app developer never sees. - [App Clips](https://nerdy.pro/glossary#app-clips): An Apple feature that runs a small slice of an iOS app — under 15 MB — without installing the whole thing. Invoked from a QR code, an NFC tag or a link, for when the first thing a user does should not require a store visit. - [BigQuery](https://nerdy.pro/glossary#bigquery): Google Cloud's analytics warehouse. You point SQL at billions of rows and it scans them in seconds, on storage held separately from the machines doing the querying — which is what keeps reporting and exploration off the database that is serving live traffic. - [CI/CD](https://nerdy.pro/glossary#ci-cd): Automation that builds, tests and ships every change without anyone running commands by hand. On mobile it is what turns a release into a button press instead of an afternoon of someone else being unavailable. - [Container registry](https://nerdy.pro/glossary#container-registry): A hosted store for container images, addressed by name and tag — Docker Hub, GitHub Container Registry, or a cloud provider's own. Pushing a build there turns 'works on my machine' into an image anyone can pull and run unchanged. - [CRUD](https://nerdy.pro/glossary#crud): Create, read, update, delete — the four operations behind almost every form and admin screen. Shorthand for the routine data-management half of an app, as opposed to the parts carrying real domain logic. - [Deep linking](https://nerdy.pro/glossary/deep-linking): A link that opens a specific screen inside an installed app instead of its home screen or a web page. Deferred deep linking survives an install, so a tap that leads through the app store still lands on the right screen. - [Dev Container](https://nerdy.pro/glossary#dev-container): A development environment described once in devcontainer.json — the OS, tools, and runtime versions a project needs — and opened identically inside a container by every contributor's editor, instead of a setup guide everyone interprets differently. - [Elasticsearch](https://nerdy.pro/glossary#elasticsearch): A search and analytics engine that indexes records so they can be filtered and searched interactively instead of scanned. What you reach for when the question is "show me these particular sessions, narrowed six ways" rather than "sum this column". - [End-to-end encryption](https://nerdy.pro/glossary#end-to-end-encryption): Encryption applied on the sending device and undone only on the receiving one, so the service carrying the message cannot read it — not under subpoena, not after a breach. It protects the content and never the metadata. - [Fan-out](https://nerdy.pro/glossary#fan-out): Reading an upstream source once and delivering each update to every client subscribed to it. The naive version writes to subscribers in a loop and stalls the moment one socket is slow; a real one buffers per client and drops whoever cannot keep up. - [Feature flags](https://nerdy.pro/glossary#feature-flags): Switches that turn functionality on or off from configuration rather than from a release. They let one binary behave differently per brand, market or user, and let a risky feature be shut off without shipping a new build through review. - [Floating point](https://nerdy.pro/glossary#floating-point): The IEEE 754 binary format behind double and float. It cannot hold 0.1 exactly, so 0.1 + 0.2 is 0.30000000000000004 — invisible in graphics and fatal in money, which belongs in integer minor units or a decimal type instead. - [GDPR](https://nerdy.pro/glossary#gdpr): The EU regulation covering personal data of people in the EU: a lawful basis for collecting it, real consent for tracking, and rights to see and delete it. It follows your users, not your servers, so it applies wherever the company is registered. - [Golden test](https://nerdy.pro/glossary#golden-test): A test that renders a widget and compares the result pixel for pixel against a stored reference image. In Flutter it is the cheapest way to answer whether a redesign broke the empty state at 320pt, in dark mode, at 200% text scale. - [GraphQL](https://nerdy.pro/glossary#graphql): A query language for APIs where the client names exactly the fields it wants and gets one response shaped to match. It removes the over-fetching REST endpoints drift into, and adds a failure mode of its own: an unbounded query that walks the whole data model. - [Headless CMS](https://nerdy.pro/glossary#headless-cms): A content system with an editor and an API but no front end of its own. Editors publish in one place, and the site or app renders that content itself — so the presentation layer is yours rather than the CMS vendor's. - [HIPAA](https://nerdy.pro/glossary#hipaa): The US law governing protected health information — how it may be stored, transmitted, logged and disclosed. Like PCI-DSS it is an architectural constraint chosen at the start, not a policy document added before launch. - [Host Card Emulation](https://nerdy.pro/glossary#host-card-emulation): Letting an Android phone act as a contactless card over NFC in software, with no hardware secure element. It is how a wallet app pays at a terminal: the phone speaks the same EMV contactless protocol the plastic card would have. - [Impeller](https://nerdy.pro/glossary#impeller): The rendering engine Flutter uses today, default on iOS since 2023 and on Android since 2024. It compiles its shaders ahead of time instead of during the first animation, which removed the shader-compilation jank that was Flutter's most visible production problem. - [In-app purchase](https://nerdy.pro/glossary#in-app-purchase): Selling digital goods or a subscription through the Apple or Google billing that both stores require for digital content and take a commission on. The hard part is never the purchase; it is restoring it on a new device and keeping entitlement state honest. - [Install Referrer](https://nerdy.pro/glossary#install-referrer): A Google Play API that hands a freshly installed Android app the campaign parameters from the link that led to the install. The Android half of deferred deep linking, and the dependable way to attribute where a user came from. - [JWT](https://nerdy.pro/glossary#jwt): A signed token carrying its own claims, so a server can tell who a request belongs to without looking a session up. Standard for mobile authentication. The signature proves it was not altered; it does not hide what is inside. - [Kotlin Multiplatform](https://nerdy.pro/technologies/kmp): Sharing business logic written in Kotlin across Android, iOS and the server while each platform keeps its own native UI. The alternative to Flutter when the interface has to be native but the rules behind it do not. - [KYC](https://nerdy.pro/glossary#kyc): Know Your Customer — the identity checks a regulated financial product runs before it lets anyone move money: document capture, liveness, sanctions and anti-money-laundering screening. It shapes onboarding more than any design decision does. - [Multi-architecture image](https://nerdy.pro/glossary#multi-arch-image): A single image tag that resolves to different binaries per CPU architecture — linux/amd64 and linux/arm64 are the common pair — so the same docker pull works unchanged on Intel/AMD servers and Apple Silicon laptops. - [Multi-tenancy](https://nerdy.pro/glossary#multi-tenancy): One deployment serving many customers, each seeing only its own data, configuration and enabled features because tenant context is resolved per request. It is what makes a fleet of branded apps one product instead of many forks. - [MVP](https://nerdy.pro/glossary#mvp): The smallest version of a product that can go in front of real users and still answer the question you built it to answer. A decision about scope, not about quality — an MVP still has to work. - [OAuth](https://nerdy.pro/glossary#oauth): The standard behind Sign in with Apple, Google and the rest: the user authorises your app at a provider they already trust, and your app receives a token instead of their password. Nobody invents a new credential and you never store one. - [Object storage](https://nerdy.pro/glossary#object-storage): Storage that holds a whole file under a key rather than in a filesystem tree — Amazon S3 and the many services that speak its API. Cheap, effectively unlimited, and the usual home for raw events, backups and media: written once, read rarely, kept forever. - [PCI-DSS](https://nerdy.pro/glossary#pci-dss): The card industry security standard binding anyone who stores, processes or transmits card data. Most apps stay out of its scope on purpose, by handing card entry to a certified payment provider instead. - [Platform channels](https://nerdy.pro/glossary#platform-channels): The bridge a Flutter app uses to call native iOS and Android code — Keychain and Keystore, biometrics, payment sheets, any SDK without a Dart package. Routine work but real work, and the first place an engineer who never left Dart will stall. - [Post-quantum cryptography](https://nerdy.pro/glossary#post-quantum-cryptography): Encryption algorithms built to stay secure against a future quantum computer, now standardized by NIST. The migration is urgent ahead of the hardware because traffic captured today can be decrypted once such a machine exists. - [Product-market fit](https://nerdy.pro/glossary#product-market-fit): The point at which a product has demonstrably found people who want it — they use it, come back, and pay. Before it, engineering answers a question; after it, engineering answers demand. - [Prompt injection](https://nerdy.pro/glossary#prompt-injection): An attack where text supplied by a user is read by a language model as instructions rather than as data, steering it past its own rules. The LLM-era sibling of SQL injection, and it appears wherever user input is concatenated into a prompt. - [Pub/Sub](https://nerdy.pro/glossary#pub-sub): A messaging pattern where a producer publishes an event and any number of consumers read it independently, with a broker in between. The producer never waits for them, which is how a request path stays fast while slower work happens behind it. - [Rate limiting](https://nerdy.pro/glossary#rate-limiting): A cap on how many requests one caller may make in a given window. It is what stops a single enthusiastic user, a scraper or a bot from spending a month of paid API budget in an afternoon, and it has to live on your side of the integration. - [REST](https://nerdy.pro/glossary#rest): The conventional style for HTTP APIs: a URL names a resource and the HTTP verb says what to do with it. The default way an app talks to a backend, and what most third-party integrations expect to find. - [SaaS](https://nerdy.pro/glossary#saas): Software sold as an ongoing subscription to a hosted product rather than as a one-off license the customer installs and runs. The vendor operates the servers, ships updates continuously, and bills per seat or per usage. - [Server-Sent Events](https://nerdy.pro/glossary#server-sent-events): A one-way stream from server to client over an ordinary HTTP connection. Simpler than a WebSocket and enough wherever only the server has something to say — a progress feed, an AI response arriving token by token. - [Server-side rendering](https://nerdy.pro/glossary#server-side-rendering): Building a page as finished HTML on the server, so the first response already carries the content, headings, meta tags and structured data. Crawlers, link previews and slow devices read it without running JavaScript. - [Skia](https://nerdy.pro/glossary#skia): The open-source 2D graphics library from Google that draws Chrome, Android and — until Impeller — every Flutter frame. It renders to PDF as well as to a screen, which is what print-to-PDF in Chrome is doing. - [SOC 2](https://nerdy.pro/glossary#soc-2): An external auditor report on how an organisation handles customer data — security, availability, confidentiality — rather than a certificate you buy. Enterprise buyers ask for it, and it constrains architecture long before the audit itself does. - [Staff augmentation](https://nerdy.pro/glossary#staff-augmentation): A hiring model where engineers from an outside partner join your team and work under your management — in your repository, your sprints, your process — instead of delivering a project of their own. You buy capacity; the code and the context stay with you. - [Staged rollout](https://nerdy.pro/glossary#staged-rollout): Releasing a build to a small percentage of users first and widening only once the crash-free rate holds. A bad build caught at ten percent is a bad afternoon; the same build at a hundred percent is a bad week. - [State management](https://nerdy.pro/glossary#state-management): How an app decides where a value lives, who is allowed to change it, and which parts of the screen redraw when it does. In Flutter the choice between Riverpod, BLoC and Provider is among the first architectural decisions and the hardest to revisit. - [Time to market](https://nerdy.pro/glossary#time-to-market): How long it takes to get a product from decision to real users. Most stack and scope arguments are really arguments about this number, because every week saved is a week of revenue, feedback and competitive position. - [TLS](https://nerdy.pro/glossary#tls): The encryption layer underneath HTTPS. It proves the server is who its certificate says, agrees a fresh key for the session, and encrypts everything after that — so the network in between sees ciphertext it cannot quietly alter. - [Total cost of ownership](https://nerdy.pro/glossary#total-cost-of-ownership): What a product costs across its whole life rather than to build once: maintenance, upgrades, annual OS and store migrations, and the second team you staff to keep two codebases in step. Usually larger than the build quote, and almost never inside it. - [WebRTC](https://nerdy.pro/glossary#webrtc): The browser and mobile standard for sending audio, video and data directly between two devices, with servers involved only in introducing them to each other. It is what an in-app video call is built on when it is not a rented SDK. - [WebSocket](https://nerdy.pro/glossary#websocket): A protocol that holds one connection open between client and server so either side can send at any moment, instead of the client asking over and over. What live prices, chat and presence indicators run on. - [White-label](https://nerdy.pro/glossary/white-label): One product shipped under many brands. A white-label mobile platform builds each client a store-ready app with its own name, design and content from a single shared codebase, instead of forking the project per customer. - [XMPP](https://nerdy.pro/glossary#xmpp): An open, federated messaging protocol, and the long-standing alternative to writing a chat backend or renting one. It extends to presence, typing indicators and file transfer, and it is old enough that every platform has a mature client library. ## Contact - Website: https://nerdy.pro - Email: welcome@nerdy.pro - Phone: +16582275716 - Phone: +995511400231 - Telegram: https://t.me/nerdypro - WhatsApp: https://wa.me/79269263946 - Russian site: https://ru.nerdy.pro - Spanish site: https://es.nerdy.pro - Dutch site: https://nl.nerdy.pro