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, <pin-set> 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 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.

targetSdkSystem CAsUser-installed CAsCleartext HTTP
≤ 23trustedtrustedallowed
24–27trustednot trustedallowed
≥ 28trustednot trustedblocked 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:

FromMaximum lifetimeRotations per year
before March 2026398 days~1
15 March 2026200 days~2
15 March 2027100 days~4
15 March 202947 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:

openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/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:

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.

<!-- res/xml/network_security_config.xml -->
<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">api.example.com</domain>
    <pin-set expiration="2027-03-01">
      <!-- live key -->
      <pin digest="SHA-256">YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</pin>
      <!-- offline backup key, never yet used to serve traffic -->
      <pin digest="SHA-256">sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

The iOS equivalent, which is declarative and enforced by the OS:

<!-- Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSPinnedDomains</key>
  <dict>
    <key>api.example.com</key>
    <dict>
      <key>NSIncludesSubdomains</key><true/>
      <key>NSPinnedLeafIdentities</key>
      <array>
        <dict><key>SPKI-SHA256-BASE64</key><string>YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</string></dict>
        <dict><key>SPKI-SHA256-BASE64</key><string>sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=</string></dict>
      </array>
    </dict>
  </dict>
</dict>

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.

LevelRotation costWhat you are trustingVerdict in 2026
LeafFree with a same-key renewal; a release otherwiseExactly one keyWorkable only with backup pins
IntermediateUnpredictableEverything that intermediate issuesOff the table on public CAs
Public rootRareEverything that CA will ever issueSo broad it is nearly pointless
Your own private rootYou decideYour own PKIThe 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 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 , 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 <pin-set> 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, 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:

// 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 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 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
  • 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
  • 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
  • Ryan Sleevi on net-dev@chromium.org, March 2021 — the private-CA quote in full: groups.google.com
  • OWASP Pinning Cheat Sheet — "The answer to this is probably never", and the do-not-pin list: cheatsheetseries.owasp.org
  • 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
  • CA/Browser Forum ballot SC-081v3 — the 398 → 200 → 100 → 47 schedule, filed by Apple, passed 11 April 2025: cabforum.org
  • Let's Encrypt, "Generation Y" — six intermediates, chosen at random "to discourage intermediate key pinning": letsencrypt.org
  • Cloudflare — the twelve unauthorized 1.1.1.1 certificates, and Fina's absence from the mobile root stores: blog.cloudflare.com
  • Flutter issue #96722 — open since January 2022: github.com/flutter/flutter
  • dio, IOHttpClientAdapter — where validateCertificate actually runs, in source: github.com/cfug/dio
  • Dart X509Certificate — the full list of what it exposes, with no public key on it: api.dart.dev
  • cronet_http, CronetEngine.build — the parameter list, bypass included and pinning absent: pub.dev
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.
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.
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.
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.
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.
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.
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, and the cryptography underneath it in how encryption works. This article is the companion to our video episode on the same subject; the previous one was 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, a Flutter-first agency that builds and maintains apps across fintech, healthcare, and retail. We also do Flutter app development for teams who would rather not find this out in production.