Licensing & Metering
How CoreSDK license tokens work, plan enforcement, usage metering, and air-gap-compatible key rotation.
Licensing & Metering
Phase 2 feature. Licensing and metering are not yet available. They ship in Phase 2. The API documented below reflects the planned design.
CoreSDK enforces plan limits and usage metering using PKI-signed license tokens (JWS, RS256/ES256). The engine verifies the token locally against an embedded public key — no outbound call is required at runtime. This means licensing works fully offline and in air-gapped environments.
How license tokens work
A license token is a JSON Web Signature (JWS) containing your plan entitlements:
{
"iss": "https://licensing.coresdk.io",
"sub": "org_acme",
"iat": 1710000000,
"exp": 1741536000,
"plan": "team",
"entitlements": {
"max_tenants": 50,
"max_rps": 5000,
"features": ["feature_flags", "audit_trails", "saml", "scim"]
}
}The token is signed with CoreSDK's RS256 private key. The embedded public key in the engine binary verifies the signature locally on every startup — no network call is made. If the token is expired or the signature is invalid, the engine refuses to start.
Configuring the license token
license:
token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."CORESDK_LICENSE_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...In SDK code:
use coresdk_engine::CoreSDK;
let sdk = Engine::from_env()?
.license_token(std::env::var("CORESDK_LICENSE_TOKEN")?)
.build()
.await?;import os
from coresdk import CoreSDKClient, SDKConfig
sdk = CoreSDKClient(SDKConfig(
sidecar_addr="[::1]:50051",
tenant_id=os.environ.get("CORESDK_TENANT_ID", "acme-corp"),
fail_mode="closed", # Phase 2: license enforcement
))sdk, err := coresdk.New(coresdk.Config{
LicenseToken: os.Getenv("CORESDK_LICENSE_TOKEN"),
})const sdk = await CoreSDK.create({
licenseToken: process.env.CORESDK_LICENSE_TOKEN!,
});Plan enforcement
When a request would exceed a plan limit, CoreSDK rejects it with an RFC 9457 error before the handler runs:
{
"type": "https://errors.coresdk.io/tenant/plan-limit",
"title": "Plan Limit Exceeded",
"status": 403,
"detail": "Your plan allows 50 tenants. Current count: 50.",
"trace_id": "01HX7KQMB4NWE9P6T2JS0RY3ZV"
}Limits enforced at runtime:
| Limit | Description |
|---|---|
max_tenants | Maximum number of active tenants |
max_rps | Requests per second (approximate; token bucket per sidecar) |
features | Feature gates — requests to disabled features return 403 |
Usage metering
CoreSDK emits usage counters as OTel metrics, exported via OTLP alongside your application metrics:
| Metric | Type | Description |
|---|---|---|
coresdk.license.requests_total | Counter | All requests, labelled by tenant and plan |
coresdk.license.tenants_active | Gauge | Currently active tenant count |
coresdk.license.feature_blocked_total | Counter | Requests blocked by feature gate |
coresdk.license.days_until_expiry | Gauge | Days until the license token expires |
These metrics feed into your existing dashboards. Set an alert on coresdk.license.days_until_expiry < 30 to get advance warning before a token expires.
Token rotation
Tokens have an exp claim. Rotate before expiry:
- Download a new token from the CoreSDK dashboard (SaaS) or generate one with the CLI (self-hosted).
- Update
CORESDK_LICENSE_TOKENin your environment or secret store. - The sidecar picks up the new token on the next config sync without a restart. Tokens can also be hot-reloaded by sending
SIGHUPto the sidecar process.
# Verify the new token before deploying
core license verify --token "eyJhbGci..."
# Output:
# Plan: team
# Org: acme
# Expires: 2027-03-19 (365 days)
# Tenants: 50 max
# Features: feature_flags, audit_trails, saml, scim
# Signature: valid (RS256)Air-gapped rotation
In environments with no outbound access, new tokens are delivered as signed files:
# On a machine with internet access:
core license download --org acme --output license.jwk
# Transfer license.jwk to the air-gapped host (USB, internal artifact store, etc.)
# On the air-gapped host:
core license install --file license.jwk
# Writes token to /etc/coresdk/license.token
# Sidecar picks up on next SIGHUP or restartThe token file is itself a JWS — the engine verifies the signature before accepting it. A tampered or invalid file is rejected with a startup error.
Checking license status
core license status
# Output:
# Plan: team
# Status: active
# Expires: 2027-03-19 (365 days)
# Tenants in use: 12 / 50
# RPS limit: 5000
# Features enabled: feature_flags, audit_trails, saml, scimProgrammatically:
let status = sdk.license().status().await?;
println!("Days until expiry: {}", status.days_until_expiry);
println!("Tenants: {}/{}", status.tenants_active, status.tenants_max);status = await sdk.license.status()
print(f"Days until expiry: {status.days_until_expiry}")
print(f"Tenants: {status.tenants_active}/{status.tenants_max}")status, err := sdk.License().Status(ctx)
fmt.Printf("Days until expiry: %d\n", status.DaysUntilExpiry)
fmt.Printf("Tenants: %d/%d\n", status.TenantsActive, status.TenantsMax)const status = await sdk.license.status();
console.log(`Days until expiry: ${status.daysUntilExpiry}`);
console.log(`Tenants: ${status.tenantsActive}/${status.tenantsMax}`);Rust API
The coresdk-license crate exposes the verification and plan-check APIs used internally by the engine.
use coresdk_license::{LicenseVerifier, LicenseToken, Plan};
// Build a verifier with the embedded RS256 public key
let verifier = LicenseVerifier::new();
// Parse and verify a JWS-encoded license token
// No outbound call is made — verification is fully offline
let token: LicenseToken = verifier.verify_jws(&raw_jws)?;
// Inspect the plan tier
match token.plan {
Plan::Free => { /* community limits apply */ }
Plan::Team => { /* up to 50 tenants, 5,000 RPS */ }
Plan::Enterprise => { /* unlimited tenants, custom RPS, HSM support */ }
}
// Check a specific entitlement gate
if token.entitlements.features.contains("audit_trails") {
// audit trail sink is permitted
}LicenseVerifier::verify_jws() validates the RS256 signature, checks the exp claim, and returns a typed LicenseToken. It never makes a network call — the public key is embedded in the binary at compile time.
Plan tiers
| Tier | Plan variant | Max tenants | Max RPS |
|---|---|---|---|
| Free | Plan::Free | 3 | 500 |
| Team | Plan::Team | 50 | 5,000 |
| Enterprise | Plan::Enterprise | unlimited | custom |
Next steps
- Configuration Reference —
license.*config keys - Air-Gapped Deployment — offline license delivery workflow
- Quotas & Rate Limits — per-tenant RPS enforcement
- Roadmap — licensing is a Phase 2 feature