Key types
A key type is a variant of an app. Every license belongs to exactly one key type, and the key type config is baked into the license record at mint time.
Why they exist
Section titled “Why they exist”An app is the thing you sell (e.g. “Gemstone”). A key type is a SKU of that app:
- Gemstone Lifetime - 3 devices, no expiry, $99.
- Gemstone 1-Year - 3 devices, 365 days, $39.
- Gemstone Family Pack - 8 devices, no expiry, $149.
Instead of three separate apps with three separate Ed25519 keysets and three dashboards, you have one app with three key types.
{ keyTypeId: string; // slug derived from displayName, e.g. "1-year" displayName: string; // shown in UI activationLimit: number; // max concurrent active devices durationDays: number | null; // null = lifetime priceInCents?: number; // for Stripe Connect priceCurrency?: 'usd' | 'eur' | 'gbp'; stripePriceId?: string; // populated by Connect entitlements?: string[]; // app-readable flags/features}Every app has at least one key type. Keylight creates a default one named Default (activationLimit: 3, durationDays: null) when you add an app.
Each key type row in the dashboard shows a small colored dot next to its name so you can tell duration at a glance:
- 🟢 Green — Lifetime (
durationDays: null). The license never expires. - 🔵 Blue — Time-limited (
durationDaysis a positive integer). The license expires after that many days from mint time.
It’s a visual shorthand for the same durationDays field shown in the row’s duration label.
What Keylight enforces
Section titled “What Keylight enforces”Two hard gates, both read from the key type config stored in the license record - not from the current app config:
- Activation cap.
/activateincrements the instance count for the license. If it would exceedactivationLimit, Keylight returns402. - Expiry. If
durationDaysis non-null, Keylight writesexpiresAt = mintedAt + durationDays * 24h. Past that,/activateand/validatereturn422with anexpiredlease.
Because these are baked into the record at mint time, editing a key type later does not change any already-issued licenses.
App flags / entitlements
Section titled “App flags / entitlements”Key types can also carry optional app flags / entitlements. These are short strings your app reads from the SDK after activation or validation.
Use them for anything the app should decide locally:
- Show a Beta badge for beta tester licenses.
- Unlock a feature such as
export,sync, orteam. - Enable early-access UI for
early_accesscustomers. - Tag reviewer, student, internal, or partner licenses without creating a separate app.
Example key type:
{ keyTypeId: 'beta', displayName: 'Beta Tester', activationLimit: 1, durationDays: 90, entitlements: ['beta', 'early_access']}In Swift, read those flags from LicenseManager:
if licenseManager.hasEntitlement("beta") { BetaBadge()}
if licenseManager.hasEntitlement("export") { ExportButton()}The dashboard labels this field App flags / entitlements because the same signed values can be used for both UI customization and feature gating.
Naming rules
Section titled “Naming rules”Flags must be lowercase and may contain numbers, hyphens, or underscores:
- Good:
beta,early_access,pro,team-sync - Avoid:
Beta,Early Access,beta!
Each key type can include up to 32 flags. Duplicates are accepted but should be avoided.
Security model
Section titled “Security model”Entitlements are included in the signed offline lease. That means the SDK can keep exposing them while offline, and local tampering breaks signature verification.
Your app still owns the behavior: Keylight does not decide what a beta flag looks like. Your code maps flags to UI, feature access, or messaging.
Pricing and Stripe
Section titled “Pricing and Stripe”When you set priceInCents and priceCurrency on a key type and you’re linked via Stripe Connect, Keylight automatically creates a Stripe Product and Price on your connected account and stores the resulting stripePriceId on the key type. You do not create products or prices in Stripe yourself, and you do not create Stripe Payment Links — both are handled (or, in the case of Payment Links, deliberately not used) so Keylight can attach the metadata it needs to mint the right license on checkout.session.completed.
That stored Price ID is what gets passed to POST /api/:tenantId/checkout when your backend mints a Checkout Session.
Editing the price archives the old Stripe Price and creates a new one. Existing subscriptions aren’t migrated — Stripe handles that on renewal.
The dashboard shows the live Stripe linkage on the key types page: each priced key type gets a status badge (Active / Active unverified / Missing on Stripe / No price). Click Verify all on the Stripe Pricing card to confirm each stored Price ID still exists and is active on your connected account — useful after manual edits in the Stripe dashboard or after reconnecting to a different account.
Hand-issued (non-Stripe) key types
Section titled “Hand-issued (non-Stripe) key types”For test, beta, gift, comp, or VIP keys you issue manually rather than selling through Stripe, tick Hand-issue only on the key type. The effects:
- Skipped from Stripe Price sync — no Stripe Product or Price is created for it.
- Excluded from the Stripe Pricing table’s status checks (shows as Manual instead of “Missing on Stripe”).
- Suppresses the Setup Incomplete banner — Keylight understands you mean for it to be off-Stripe.
- The Checkout API (
POST /api/:tenantId/checkout) returns 422 for it, so a customer cannot accidentally buy a hand-issued tier. - You can still set
priceInCentsfor record-keeping (the badge becomes “Manual (hand-issued, $X intent)”) — useful when the key has a notional value but you’re invoicing or comping outside Stripe.
Toggling Hand-issue only ON for a key type that already has a Stripe Price archives that Price on Stripe and clears the local stripePriceId. Toggling it OFF on a priced key type creates a fresh Stripe Price the next time you save. Existing licenses of that key type are unaffected — only the sales/checkout path changes.
Subscription key types
Section titled “Subscription key types”A key type can be set to billingModel: 'subscription' to indicate that licenses of this type are tied to an active subscription. For these keys, expiry is driven by the payment platform rather than by durationDays.
{ keyTypeId: 'pro-monthly', displayName: 'Pro (Monthly)', activationLimit: 3, durationDays: null, // ignored for subscription keys billingModel: 'subscription', graceDays: 7, // added to period_end on issuance + every renewal (default 7) entitlements: ['pro']}| Field | Type | Default | Description |
|---|---|---|---|
billingModel | 'one_time' | 'subscription' | 'one_time' | Switches the key into subscription mode. |
graceDays | number | null | 7 | Days added to the subscription’s current_period_end when computing expiresAt — applied on initial issuance and every renewal, not only on failure. This buffer keeps the license alive briefly after a failed renewal while payment recovers. Only relevant when billingModel is 'subscription'. |
Expiry is webhook-driven. When a renewal webhook arrives, Keylight extends expiresAt to new_period_end + graceDays on the license record. The device picks up the extended expiry on its next lease refresh — no re-activation needed.
Renewal failure. A past_due event marks the subscription status on the license record but leaves expiresAt at period_end + graceDays. If payment recovers within the grace window, the subscription status flips back to active and the license keeps working. If the grace window elapses with no recovery, the license expires normally.
Cancellation. A canceled event records the status and leaves expiresAt at the already-known period end. The license expires naturally when that point passes.
Reviving a lapsed subscription key
Section titled “Reviving a lapsed subscription key”When a customer with a fully lapsed subscription key subscribes again, Keylight revives the same key rather than minting a new one. To make revival unambiguous, the re-subscribe checkout should carry keylight_renew_key metadata (the existing key string). Keylight verifies the key belongs to the same product, flips it back to active, links the new subscription ID, and extends expiresAt.
If the metadata is absent, Keylight falls back to matching on the payment-platform customer ID. It revives the key only when exactly one lapsed subscription license matches — zero or multiple matches result in a fresh mint instead, to avoid any entitlement ambiguity.
Limited access (fallback)
Section titled “Limited access (fallback)”The fallbackAccess toggle on a key type controls what happens when a license of that type expires.
| Field | Type | Default | Description |
|---|---|---|---|
fallbackAccess | boolean | null | false | When true, an expired license resolves to .limited instead of .expired. |
When fallbackAccess is true and a license expires:
/activateand/validatestill return a valid signed lease, but withstatus: "fallback"and an empty entitlements list. No paid features are included.- The Swift SDK maps
status: "fallback"to the.limitedstate. - The lease keeps its normal 7-day TTL, so the SDK continues re-verifying weekly and the app stays offline-capable.
When fallbackAccess is false (the default), expiry behavior is unchanged: the server returns an expired lease and the SDK resolves to .expired.
This works identically for one-time keys (expired past durationDays) and subscription keys (expired past period_end + graceDays).
Upgrading licenses
Section titled “Upgrading licenses”See Upgrades for the full upgrade flow — how to add the in-app upgrade button, configure per-provider hosted checkout URLs, and how customers recover lost keys.
Related
Section titled “Related”- Apps & key types - creating and editing from the dashboard.
- HTTP API - how activation and validation see the key type.
- License lifecycle - how
.limitedand.freeTiermap to these settings.