Open Core in a Public Repo: Gate Paid Features With License Keys, Not Secrecy
DEV Community

Open Core in a Public Repo: Gate Paid Features With License Keys, Not Secrecy

Your repository is public. Anyone can read the paid modules, fork them, and build them. So what actually separates the Community edition from the paid one?

In open core and Fair Source projects the answer is not code secrecy. It is the license key: a signed entitlement that tells one self-hosted installation which paid features it is allowed to use. This post walks through how to design that boundary so it stays fair to operators and practical for you.

Open core vs Fair Source in one paragraph

Classic open core keeps the core under an open source license and puts commercial modules in a separate directory (often ee/) with their own terms. Fair Source goes a step further: the commercial code is readable, but the license limits certain uses of current versions and converts to a permissive license later. Keygen moved to FCL, and Directus announced an FCL-derived license for v12 together with registration keys for paid users.

Quick comparison (high level, not legal advice, always read the exact terms you ship):

Model What it does
Separate ee/ terms Core and commercial modules carry different licenses; the repo makes the boundary obvious
FSL Limits competing use; becomes Apache 2.0 or MIT after two years
FCL Limits competing use, protects license-key functionality, future license per chosen variant
ELv2 Restricts offering as a managed service, bypassing key functionality, removing notices

The key is the line, and that is fine

A self-hosted operator controls the binary. They can patch it. Accept that, and design for the honest majority:

  • Sign the proof. A modified config file should never be able to grant access in the unmodified product.
  • Check where the paid operation runs, on the server side, not only in the UI.
  • No mandatory telemetry, no remote kill switch. Operators of self-hosted software care a lot about this.
  • Explain state clearly. If a feature is unavailable, say why and how to restore it. Never lock people out of their own data.

Model features as entitlements, not editions

Use stable entitlement identifiers instead of hard-coding "is this Pro?":

  • Community: read and edit data (no key needed)
  • Pro: adds exports
  • Enterprise: adds sso

Each paid operation checks its own entitlement. If the proof is invalid or scoped to another product, the paid operation is denied and Community keeps working.

using PermitCore;

static async Task<(bool Exports, bool Sso)> CheckFeatures(PermitCoreClient client, Guid productId)
{
    var key = Environment.GetEnvironmentVariable("PERMITCORE_LICENSE_KEY");
    if (string.IsNullOrWhiteSpace(key)) return (false, false);
    var result = await client.ValidateAsync(key, expectedProductId: productId);
    return (result.IsValid && result.HasFeature("exports"), result.IsValid && result.HasFeature("sso"));
}

Air-gapped and flaky-network installs

Self-hosted often means "no internet". Two patterns cover most cases:

  • Air-gapped: ship a signed token file, ship trusted public keys through your release channel, and validate offline (signature, deadline, device, expected product).
  • Normally connected: configure an offline grace period so signed cached proof keeps working through a network outage.
static bool AcceptOffline(PermitCoreClient client, string deviceId, Guid productId)
{
    var path = Environment.GetEnvironmentVariable("PERMITCORE_OFFLINE_FILE");
    if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return false;
    return client.ActivateOffline(File.ReadAllText(path).Trim(), deviceId, productId).IsValid;
}

Let customers keep what they paid for

If your terms promise a perpetual fallback, keep that decision separate from license validation: compare the chosen build's release date with a trusted maintenance end date.

static bool CanUseRelease(LicenseResult entitlement, DateTime releasedAtUtc) => entitlement.IsValid && (entitlement.MaintenanceExpiresAt is not DateTime end || releasedAtUtc <= end);

Docker, Kubernetes and CI

Never bake the key into an image layer.

  • Docker: docker run --env PERMITCORE_LICENSE_KEY ... (inherits from the exported variable)
  • Kubernetes: env.valueFrom.secretKeyRef on the workload
  • CI: map the protected secret to the same variable for integration tests
  • Offline installs: mount the token file read-only and point PERMITCORE_OFFLINE_FILE at it

Keep keys and token files out of build logs and public CI output.

Wrapping up

The examples use the PermitCore .NET SDK, which handles key issuing, entitlements and online/offline validation. The full guide, with links to the offline, grace-period and key-rotation deep dives, is on our blog: Open Core and Fair Source: gating paid features with license keys.

If you ship open core or Fair Source: where do you draw the line between Community and paid, and do you check entitlements in the UI, the server, or both? 🙂

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.