Skip to main content
Migrate Already selling? Move your customers to Keylight without re-issuing a single key.
Keylight
Blog
analytics telemetry licensing

Which Version and Platform Are Your Paying Users Actually On?

9 min read Nicolas Demanez — Founder

You are about to drop support for an OS version. How many paying customers are still on it?

Most indie desktop developers cannot answer that. They guess from download counts, from the last few support emails, or from what their own machine runs. Then they either drop support too early and eat a week of refund requests, or they carry a compatibility branch for two years to serve eleven people.

That is a decision that wants a number, and the number is cheap to get. Your licensing layer already talks to your server. Every activation and every validation is a network call that is happening whether you instrument it or not. App version, OS major version, and platform can ride along on that call. No extra SDK, no extra request, no extra infrastructure.

This post is about that specific slice: version and platform segmentation of your paying base. The broader set of numbers worth watching is covered in the 9 licensing metrics every app business should track.

The four decisions version and platform data actually drives

Four decisions, and all four are expensive to get wrong.

When to drop an old OS version. Every OS you support is a build target, a test matrix row, and a set of API workarounds. Dropping one is pure margin. But drop it while 9% of your paying base is still there and you have just broken your app for one in eleven customers who paid you. With adoption data this stops being a vibe and becomes a threshold you set once and check quarterly.

Whether a bug affects paying users or only free ones. A crash report says 1,400 sessions hit a null reference. That tells you nothing about urgency. Segmenting by license state tells you whether those sessions are trial users on a version you already patched, or your top 30 paying accounts on the current build. The second one ships tonight. The first one waits for the next scheduled release.

Whether an update is being adopted at all. You shipped 3.2 six weeks ago. If 70% of paying installs are on it, your updater works. If 12% are on it, your updater is broken, or the update prompt is too easy to dismiss, or people genuinely do not want it. Those are three different problems and you cannot tell them apart without the curve.

Which platform deserves the next feature. If your Windows base is 40% of paid activations and gets 10% of your engineering time, that is a decision you should be making deliberately rather than by habit. Teams shipping to more than one desktop platform run into this constantly, which is part of why one control plane across platforms matters more than it sounds like it should.

Notice what is not on that list: growth reporting. This data is for engineering and support decisions. Treat it as a marketing dashboard and you will start collecting fields you do not need.

Why downloads by platform is the wrong denominator

Downloads measure intent. Paid activations measure usage. They differ by more than you expect.

A download count includes evaluators who opened the app once, people who downloaded on two machines and bought on neither, mirrors, crawlers, and everyone re-downloading after a reinstall. The population that downloads and the population that pays have visibly different platform mixes. Linux and Windows tend to over-index on downloads relative to paid conversion. macOS tends to under-index on downloads and over-index on revenue. Plan your roadmap off downloads and you will over-serve the platform that evaluates the most and under-serve the one that pays.

Release-page download counters have a second problem: they are cumulative and they never expire. A version released 18 months ago keeps its download number forever, so old versions look permanently popular even after every one of those installs has updated or been deleted. You cannot read an adoption curve off a counter that only goes up.

The right denominator is active paid installs in a window. Something like: distinct devices that produced a successful validation in the last 30 days, bound to a license in a paying state. That population is finite, it decays correctly as machines go away, and it is exactly the group your support obligations apply to. If you already track device binding, you already have the join key. The mechanics of that binding are in how device activations work.

One caveat. Apps with heavy offline usage undercount, because a device validating from a cached lease may not phone home for days. Read the shares, not the absolute counts, and pick a window wider than your lease duration.

What to send, and what to never send

Send four fields. Send nothing else.

  • app_version: your full version string, for example 3.2.1.
  • os_version: the major version only, for example 15 or 11. Not the build number.
  • platform: macos, windows, or linux.
  • sdk_version: the version of your licensing SDK, so you can tell whether a validation failure is a client bug.

That drives all four decisions above. Everything else is scope creep dressed as curiosity.

Here is the line I hold. Do not send user identifiers beyond the license you are already validating. Do not send file paths, document names, or project contents, ever, and that includes truncated ones and hashed ones. Do not send hardware serial numbers, MAC addresses, or anything else that follows a person across products. Do not send IP-derived geolocation into your analytics store just because the request has an IP on it. Do not send a per-install UUID that outlives the license.

This is where bolt-on analytics SDKs get apps in trouble. A general purpose product analytics SDK is built for web funnels. It collects a persistent anonymous ID by default, often grabs screen and locale and network details, and phones home on its own schedule to a third-party domain. On macOS that means a new outbound connection users will see in Little Snitch and post about. It also means a third party now holds behavioral data about your paying customers, which is a disclosure obligation you took on without meaning to.

Riding on the licensing call avoids all of that. The connection to your licensing host is already expected, already disclosed, and already scoped to a paying relationship. You are adding four strings to a request body:

// The validation call is already happening. The metadata is a payload field,
// not a second request and not a second SDK.
await keylight.validate({
  licenseKey,
  client: {
    appVersion: '3.2.1',   // full version string
    osVersion: '15',       // MAJOR ONLY. never the build number
    platform: 'macos',     // macos | windows | linux
    sdkVersion: '0.9.2',   // so client bugs are attributable
  },
})

// Not in this object, not ever:
//   userId, email, hostname, machineSerial, ipCity,
//   documentPath, projectName, featureFlags the user toggled

The Swift side is the same shape:

let result = try await keylight.validate(
    client: .init(
        appVersion: Bundle.main.shortVersion,
        osVersion: String(ProcessInfo.processInfo.operatingSystemVersion.majorVersion),
        platform: "macos",
        sdkVersion: Keylight.sdkVersion
    )
)

One more discipline point: keep the fields on the request, not in a separate analytics event. The moment version reporting becomes its own call, it becomes something that can fail independently, retry independently, and be blocked independently. Then you have a telemetry pipeline to maintain, which is the thing you were trying to avoid.

Reading an adoption curve and deciding to drop support

Adoption curves for paid desktop apps are slower than you think, and that slowness is the whole reason this data changes decisions.

Here is a real-shaped example. You ship 3.0 on March 1 with an auto-updater. Measured as share of active paid installs:

Weeks after releaseOn 3.xOn 2.xOn 1.x
134%51%15%
462%27%11%
874%17%9%
1279%13%8%
2484%9%7%

Two things jump out. The first is that the curve flattens hard around week 8. Most of the adoption you are ever going to get happens in the first month, and after that you are looking at a long tail that barely moves. The second is that the 1.x tail is not decaying. It sits around 7% to 8% forever. Those are machines with automatic updates disabled, locked-down corporate installs, and people on an OS too old to run your current build.

So what do you do at week 24 with 7% still on 1.x?

You do not drop it because six months feels like enough. You price it. Say 7% of a 4,000 install paying base is 280 customers. If supporting 1.x costs you a CI runner, a legacy build target, and roughly two days a quarter of compatibility work, that is real. If those 280 customers are perpetual-license holders who already paid and will never pay again, the math points one way. If a third of them are on active subscriptions, it points the other way, and the right move is a targeted upgrade push before a deprecation.

Concretely, a policy I would actually run: support the current major and the one before it, plus any version whose paid install share is above 5%. Announce deprecation when a version crosses below 5% and remove it 90 days later. Publish the number in the announcement. “3% of active licenses are on 1.x” is a far better deprecation notice than “1.x is now legacy” because it tells the affected person whether they are the edge case.

The same logic applies to OS versions, with one adjustment: OS tails decay even slower, because upgrading an OS sometimes means buying a computer. Give OS deprecations six months of notice, not six weeks.

Cutting a version inside the app is a licensing question too. The cleanest approach is a validation response that carries a minimum supported version, so an old client learns it is out of support from the call it is already making instead of from a crash. That server-side control over what a client is entitled to is what a license key is for, and it is the same mechanism that scopes features per plan.

Where this data should come from

You have two options, and the DIY one is genuinely fine at small scale.

Building it yourself is not hard. Add four columns to whatever table records your activations and validations. Write an aggregation query that buckets distinct devices by version over a 30 day window. Put it on a cron and email yourself the result weekly. That is maybe half a day of work and it will answer every question in this post. If you have a few hundred customers, do this and stop reading.

The part that gets expensive is later. Raw validation rows accumulate fast, so within a year you are writing a rollup job and a retention policy. Then you want the breakdown segmented by license state, so you join against subscriptions. Then you want the same view per product because you shipped a second app. None of these steps is hard. All of them are unplanned. This is the pattern where the simple thing you wrote at launch turns into a system you maintain, which is the subject of why licensing gets hard at scale.

The alternative is letting the layer that already handles activation record it. Keylight accepts app version, OS version, platform, and SDK version on activation and validation calls and segments the usage view by all four, because the request is already going through it and the license state is already there. Stripe stays your payment processor. The licensing layer answers what is running. See pricing for what that costs.

Either path, the point is the same. Stop making deprecation and triage calls off download counts and support-email impressions. Four strings on a request you are already sending is the entire cost of knowing.

Frequently asked

What is app version adoption analytics?+

It is the share of your active paying installs running each shipped version of your app, tracked over time. It answers whether an update is actually reaching people and how long old versions stay in the field before they fade out.

Can I collect desktop app telemetry without tracking users?+

Yes. Send only app version, OS major version, platform, and SDK version, attached to a license that you already identify. That is enough to drive deprecation and triage decisions and it carries no browsing history, no file paths, and no hardware serials.

Why not just use downloads by platform?+

Downloads count intent, not usage. They include evaluators who never bought, bots, mirrors, and re-downloads. Paid activations count machines that a paying customer actually runs, which is the population your support policy applies to.

How long should I support an old OS version?+

Until the paying install share on it falls below the point where supporting it costs more than the revenue it represents. For most indie desktop apps that is somewhere between 2% and 5%, but the number matters less than measuring it instead of guessing.

Does version telemetry require a privacy policy update?+

If you collect anything beyond what is strictly needed to deliver the license, disclose it. Version and platform fields tied to a license are low risk, but the honest move is one plain sentence in your policy saying exactly which fields you send.

Ready to ship?

Create your account and start licensing your apps in under a minute. Free forever tier included.

Start Free