VidViz #licensing #electron #launch

Launching a three-week-old app: licensing, packaging, and a demo that can't be broken

VidViz is 22 days old and as of this week it has a price. Not because it's finished — because a product with users finds its shape faster than a project with a roadmap. This post is about everything that isn't visuals: the licensing, the backend, the packaging, and the surprisingly philosophical question of what a demo user should be allowed to destroy.

The web side, briefly

Three deployables joined the repo: a Fastify + PostgreSQL API (auth, billing, licensing, support tickets) with Stripe for money and Resend for email; a Next.js site that is both marketing pages and user dashboard; and a separate admin app, IP-whitelisted at the proxy, with TOTP on top of login. All of it runs on a single small Hetzner VPS behind Traefik for about the price of a coffee a month. (The domain also moved — vidviz.app to vidviz.ai — early enough that nothing had to break.)

Tiers: Lite as a one-time purchase, Premium and Pro as subscriptions, and an AI module as an add-on. Lite being perpetual isn't generosity, it's positioning — a music visualiser has hobbyists, and hobbyists rightfully resent subscribing to a toy.

Licensing without trusting the client

The scheme has two layers. A license key (VVIZ-XXXXX-…) is the thing you buy: stored server-side with its tier and modules, bound to a machine fingerprint on first activation. What the app actually trusts is the second layer: on validation, the API returns a short-lived Ed25519-signed token carrying tier, modules, and machine ID. The desktop app embeds only the public key. It can verify anything and forge nothing, which is the entire design in one sentence.

This week's hardening pass was about believing my own scheme:

  • The license state moved out of the renderer entirely. Electron's renderer is a browser page — devtools-openable, script-injectable. Now the main process owns validation and pushes the tier down via IPC; the renderer can ask, never assert.
  • Verification uses Node's built-in crypto against the embedded public key — no dependency between the app and the truth.
  • The signed token is cached encrypted on disk via Electron's safeStorage, with a 7-day re-validation cadence and a matching grace period — offline for a week is fine, offline forever is a refund.
  • The machine ID became a stable hardware-derived fingerprint. The first version was a random UUID, which is to say: reinstall the app, consume another activation. A machine binding that doesn't survive reinstalls punishes exactly the honest users.

Is client-side licensing crackable? Always. The goal is a lock that keeps honest people honest and makes the dishonest path more work than $14.99.

Packaging: two runtimes in one DMG

VidViz is Electron and a Python backend — librosa, scipy, Essentia — and "please install Python 3.11 first" is not a consumer product. The answer is PyInstaller: the backend compiles to a self-contained binary (with a spec file whose hidden-imports list is its own small war story), shipped inside the DMG as an extra resource alongside an NDI dylib and the Rust cast binary. The app spawns it on launch, degrades gracefully if it fails, and the user never learns what uvicorn is. One icon, two runtimes, three languages.

The demo that can't be broken

The DMG ships a seed database: six demo tracks with their audio and their pre-computed analysis baked in, wired into demo projects. First launch is: open app, press play, something beautiful happens — no import step, no thirty-second analysis wait as the first impression, since the first impression is precisely what a beat-detection wait would ruin.

Which creates a new problem: the demo project is now load-bearing, and users poke things. So the Demo project is locked — viewable and playable, but save, rename, delete, and node edits are no-ops unless a dev flag is on, with menu items disabled to match. Fresh users get a pristine demo forever; the "make it your own" path is duplicating into your own project, which is the workflow we'd want to teach anyway. The lock is enforced in the store layer, not the UI — buttons lie, reducers don't.

Premium nodes got a similar structural treatment: locked node types render with an overlay and an upgrade prompt, gated by the tier the main process vouches for.

What launching this early actually bought

Everything in this post was invisible work — nobody buys an app for its token signing. But the exercise forced decisions that improve the app itself: the license moving to the main process cleaned up a lazy renderer-owns-everything habit; the seed database forced project import/export to actually work; PyInstaller flushed out a half-dozen lazy-import assumptions in the backend. Shipping is a linter for architecture. It found plenty.