Skip to content

Building an Edition System: License Validation and Feature Gates

Monetizing an open-core database requires a clean boundary between free and paid features. ZeptoDB’s edition system uses RS256-signed JWT license tokens, a two-tier model (Community/Enterprise), and a bitmask-based feature gate pattern that integrates at both the HTTP and engine layers.


We started with three tiers (Community/Pro/Enterprise) and quickly consolidated to two. Simpler sales motion, no mid-tier confusion, aligned with industry practice (kdb+, ClickHouse, TimescaleDB).

EditionCostFeatures
CommunityFreeCore engine, SQL, basic RBAC, single-node
EnterpriseLicensedSSO, cluster, Kafka, migration, audit export, advanced RBAC

Old JWT keys with "edition": "pro" are automatically mapped to Enterprise.


{
"sub": "license",
"edition": "enterprise",
"company": "Acme Trading",
"features": 255,
"max_nodes": 16,
"iat": 1713139200,
"exp": 1744675200
}

Key loading priority: ZEPTODB_LICENSE_KEY env var → /etc/zeptodb/license.key file → direct string via POST /admin/license. No phone-home — public key embedded at compile time, reuses existing JwtValidator RS256 infrastructure.


License Timeline:
────────────────────────────────────────────────────
│ Licensed (full features) │ Grace (30d) │ Community │
─────────────────────────────┼───────────────┼─────────────
exp date exp+30d
  • 30-day grace keeps production running during renewal
  • 7-day warning log before expiry
  • After grace: automatic downgrade to Community (no crash, no data loss)
  • No key = Community with zero log noise

Features are controlled by a bitmask in the features JWT claim:

enum class Feature : uint32_t {
SSO = 1 << 0, CLUSTER = 1 << 1,
KAFKA = 1 << 2, MIGRATION = 1 << 3,
AUDIT_EXPORT = 1 << 4, ADVANCED_RBAC = 1 << 5,
ROLLING_UPGRADE= 1 << 6,
};
{
"error": "enterprise_required",
"message": "Feature 'multi-node cluster' requires Enterprise edition",
"upgrade_url": "https://zeptodb.com/pricing"
}
bool KafkaConsumer::start() {
if (!license().hasFeature(Feature::KAFKA)) {
ZEPTO_WARN("Kafka consumer requires Enterprise license");
return false;
}
// ... normal startup
}

SSO/OIDC

Login, callback, session, refresh return 402. Logout and /auth/me remain open.

Cluster

Node management, all 6 rebalance endpoints, and join_cluster() in the engine.

Kafka/Migration

KafkaConsumer::start(), ClickHouseMigrator::run(), HDBLoader::scan() gated.

Advanced RBAC

Per-tenant isolation and tenant admin endpoints. Basic RBAC remains free.

FeatureGate LocationCommunity Behavior
SSO/OIDC4 HTTP routes402
Audit exportGET /admin/audit402
ClusterHTTP + engine402 / runtime_error
Rebalancing6 HTTP routes402
Rolling upgrade1 HTTP route402
Tenant rate limiting3 HTTP routes402
Kafka/PulsarEngineWARN + false
MigrationEngineWARN + false

generate_trial_key(company) creates an unsigned JWT (alg:none) with all features, single-node, 30-day expiry. Available via POST /admin/license/trial.

GET /api/license (no auth) returns edition, features, trial status — useful for UI feature toggles and client SDK capability detection.