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 isn't always fully static

The domain check above runs entirely offline. Confirming the token's signature first — before that check can even run — occasionally isn't, 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() (Section 4.12) — every file in the license
30// module checks against the same list.
6

After adding a domain in the portal

This one detail isn't fully nailed down yet — treat it as a debugging checklist item rather than a settled fact until it's confirmed.
typescript
1// One open question, worth treating as unconfirmed until you've
2// verified it against the current portal behavior: does adding a
3// domain in the portal RE-SIGN your project's token?
4//
5// If it does — you'll need to redeploy with the refreshed
6// FLUX_LICENSE_TOKEN value from the License tab after adding a
7// domain. The portal's success toast is expected to say so
8// explicitly if this is the case.
9//
10// Until this is confirmed either way: treat "I just added a domain
11// and it's still showing as unauthorized" as a signal to check the
12// License tab for a NEW token value first, before assuming
13// something is broken.
Unconfirmed behavior

Whether adding a domain re-signs the token is flagged as an open engineering question in the Portal PRD. Verify against current portal behavior before treating this section as settled.

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.