License

Domain binding

Every license token is signed with the domain(s) it's authorized to run on. This page covers how that check works, what a mismatch actually does to your app (spoiler: nothing breaks), and the tier rules that govern how many domains a project can carry.

Prerequisites
  • A FLUX_LICENSE_TOKEN already wired into createFlux() — see license/setup if not
1

Your token carries a signed domain list

This check runs on every boot, not once at issuance — and it includes subdomain matching so one token covers a whole subdomain footprint.
typescript
1// Every FLUX_LICENSE_TOKEN carries a signed domains[] array. This
2// isn't compared once at issuance — verifyLicense() checks it
3// against window.location.hostname on EVERY boot, with subdomain
4// matching built in:
5//
6// domains: ["myapp.com"]
7//
8// myapp.com -> match (exact)
9// api.myapp.com -> match (subdomain)
10// admin.myapp.com -> match (subdomain)
11// otherapp.com -> NO match
12//
13// An entry authorizes the exact domain AND anything ending in
14// "." + that domain — so one token covers your whole subdomain
15// footprint without needing separate issuance per subdomain.
Verifying the signature is not always fully static

The domain check above runs entirely offline. Confirming the token's signature first — before that check can even run — occasionally is not, for project-scoped tokens. Details below.

typescript
1// One nuance the domain check above doesn't cover: BEFORE any
2// domain/expiry check can run, Flux has to confirm the token's
3// signature is genuine — and that step needs the right public key.
4//
5// Legacy tokens (no project_id embedded) — verified against a
6// single master public key baked into the package at build time.
7// Fully offline, no fetch, ever.
8//
9// Project-scoped tokens (the current default — every token issued
10// from a project's License tab carries its own project_id and
11// key_version) — verified against THAT PROJECT'S OWN public key,
12// fetched from:
13//
14// https://api.tsworldtechflux.dev/v1/keys/{projectId}?v={version}
15//
16// This fetch happens at most once every 3 days per project, per
17// browser — the result is cached in IndexedDB with a 3-day TTL.
18// Inside that window, verification (signature, domain, expiry — all
19// of it) is fully offline. Outside it, one small request goes out
20// before the domain check above even runs. If that request fails
21// (offline, network blip), Flux falls back to the last cached key
22// rather than failing the check outright — so a temporary network
23// issue never breaks a previously-working session.
24//
25// This isn't a contradiction of "offline license verification" —
26// the domain/expiry/signature LOGIC never touches the network; only
27// fetching the public key material occasionally does, the same way
28// any signature scheme needs the verifying key from somewhere.
2

How many domains, per tier

The same rules as the portal's Add Domain flow, restated for reading rather than clicking.
typescript
1// Same rules as the portal UI, restated for someone reading docs
2// instead of clicking through Add Domain:
3
4// starter
5// Exactly 1 domain per project.
6// Its own subdomains are covered automatically — no separate
7// registration needed for api.myapp.com if myapp.com is the
8// registered domain.
9
10// pro
11// Unlimited domains, but every domain after the first must be a
12// subdomain of that first domain.
13// myapp.com, then api.myapp.com -> fine
14// myapp.com, then otherapp.com -> rejected at Add Domain time
15
16// enterprise
17// Unlimited domains, any root domain, no relationship required
18// between them. myapp.com and totally-different-app.com can both
19// live on the same project.
3

What actually happens on a mismatch

Not an error, not a thrown exception, not a blocked app — three specific, silent things happen instead.
typescript
1// What actually happens when window.location.hostname doesn't
2// match domains[] — this is NOT an error, NOT a thrown exception,
3// and NOT a blocked page. Three things happen, all silent from the
4// end user's perspective:
5
6// 1. Silent downgrade — Pro/Enterprise features simply don't unlock
7// for this session. The app keeps running at free-tier behavior.
8
9const state = verifyLicense(token)
10// state.valid === false
11// state.reason === 'DOMAIN_MISMATCH'
12
13// 2. Console warning — names the domain the token was actually
14// authorized for. Visible to whoever opens devtools on the
15// unauthorized domain, including a thief inspecting their own
16// deployment.
17
18// 3. Webhook beacon — fires to YOUR OWN webhook URL (embedded and
19// signed inside the token itself), NOT to any TsWorldTech
20// endpoint. This is the only LicenseInvalidReason that ever
21// triggers a beacon — see license/webhook_setup for what the
22// payload contains and how to receive it.
4

Why silent downgrade instead of a hard block

A deliberate tradeoff, not an oversight — worth understanding before assuming this should behave more strictly.
typescript
1// Why silent downgrade instead of a hard block:
2//
3// A hard block on domain mismatch means one misconfigured token —
4// staging domain not yet added, token copied into the wrong env
5// var — bricks a real user's production app. Silent downgrade means
6// the worst case for a legitimate mistake is "some features are
7// off," never "the app is down."
8//
9// The tradeoff this accepts: a thief gets a console warning telling
10// them exactly which domain WOULD work, rather than a hard failure
11// with no information. That's considered an acceptable cost for
12// never bricking a legitimate customer's app over a config typo.
5

Local development is always exempt

None of the above applies during local development, staging, or preview deployments, regardless of what your token's domains[] array says.
typescript
1// Local development, staging, and preview deployments are exempt
2// from ALL of the above, regardless of what's in domains[]. Checked
3// independently of hostname via NODE_ENV === 'development', plus
4// several hostname-based checks:
5
6// Exact hosts / suffix patterns:
7// localhost
8// 127.0.0.1
9// ::1
10// *.localhost
11// *.vercel.app
12// *.netlify.app
13// *.pages.dev
14
15// Subdomain PREFIX pattern — anything starting with one of these,
16// on any domain, not just the hosts above:
17// stage.* staging.* dev.* preview.* review.*
18//
19// e.g. stage.mycompany.com or dev.internal.io are both exempt,
20// even though mycompany.com and internal.io themselves are not.
21
22// Optional build-time override, for setups the patterns above don't
23// cover (custom branch-preview domains, internal QA hosts):
24// FLUX_DEV_DOMAINS=foo.example.com,bar.example.com
25//
26// A token authorized only for myapp.com still runs at full tier on
27// any of the above — no console warning, no webhook beacon, no key
28// fetch either. This list is defined once, centrally, in
29// isDevEnvironment() — every file in the license module checks
30// against the same list.
6

After adding a domain in the portal

Treat this as a debugging checklist item rather than a settled fact until verified against current portal behavior.
typescript
1// Does adding a domain in the portal RE-SIGN your project's token?
2//
3// If it does — you'll need to redeploy with the refreshed
4// FLUX_LICENSE_TOKEN value from the License tab after adding a
5// domain. The portal's success toast is expected to say so
6// explicitly if this is the case.
7//
8// Treat "I just added a domain and it's still showing as unauthorized"
9// as a signal to check the License tab for a NEW token value first,
10// before assuming something is broken.
Token Re-signing Notice

Adding a domain to your project on the portal generates a refreshed, cryptographically signed token. Ensure you update FLUX_LICENSE_TOKEN in your target deployment environment.

Self-Contained Verification Architecture

Because domain validation is embedded inside the Ed25519-signed token envelope and verified against the local origin hostname, applications deployed inside air-gapped environments or behind strict corporate firewalls validate without external telemetry calls.

For what counts against your quota, see license/quotas. For receiving the webhook beacon mentioned in Step 3, see license/webhook_setup. For the four credentials referenced throughout, see license/setup.

Powered by Flux