Licensing

No feature is licence-gated, on any tier. Every edition runs the same binary with the same features: search, navigation, the API, every MCP tool, auditing, single sign-on through your proxy, branding. Tiers differ in exactly two numbers, how many repositories and how many users, and both come out of the licence file. Changing tier is a new licence file, not a new release.

That is a security property as much as a commercial one. A gated feature is a place where the product behaves differently depending on the licence, and the dangerous version of that is one where a licence lapse changes who can see what. There is no such place in searchcode enterprise.

Verification is offline

The licence is a signed file. It is verified against a public key compiled into the binary, with no network call, at startup. There is no licence server, no check-in, no telemetry and no phone-home, on any tier. An air-gapped install verifies its licence exactly the way a connected one does. See Trust for the full list of what the process does and does not connect to.

Where the licence goes

Either of:

When both are set, the file wins. The file is what an operator edits and the environment is what a platform injects, and a stale injected value silently overriding a deliberate edit would be the wrong way round.

In Kubernetes, put the licence in a Secret created out of band and reference it from the chart's license.existingSecret. Never put it inline in a values file. See Install with Helm and Air-gapped install.

A licence is read once, at startup. Replacing the file and reloading the configuration does not pick up a renewal; restart the process. license.file is a restart-only setting, and a reload that changes it is refused, which is the signal that a restart is the step.

No licence at all is a supported, stable way to run. It is the community edition: real limits, no expiry, no countdown.

The two meters

Repositories: warns, never blocks

The repository meter counts what is in the estate. A directory source counts units, not mounts: how a mounted directory becomes one or many searchable units is unitDetection on the source, and it is a metering input.

Crossing the licensed count logs a warning, and the count and limit are on /api/v1/status and in license show. Nothing is blocked. The repository over the limit is cloned, indexed and searchable exactly like the one before it, and the difference is reconciled at renewal.

Users: refuses only a genuinely new person

The user meter counts distinct people seen in the last thirty days. It rolls, so leavers and one-off triallists drop off without anybody pruning a list.

Every credential a person holds — a key for Claude Code, another for Cursor, an OAuth token — rolls up to that one person. One engineer with three agents is one user. A service account declared in configuration has no person behind it, so it counts as its own user.

This is the only meter that can refuse anybody, and it refuses narrowly. In order:

  1. An administrator is never refused, whatever the count. The person who can fix the problem must always be able to sign in.
  2. Anybody seen in the last twelve months is never refused. "New" means new to this instance in a year, not "outside the thirty-day count". Somebody back from a long holiday is not locked out of a tool they have used for a year.
  3. If the user store cannot be read, the meter admits, and says so loudly. An unreadable count is a fault in our own bookkeeping, and locking an organisation out of its code search over one is the worse failure. (Authentication against the same store still fails closed. This applies to the count only.)
  4. Otherwise, a person never seen before is refused when the active count is at the limit.

A refused person is told the instance is at its licensed user limit and that this is not an access-control decision, so nobody mistakes it for a permissions problem. The admin page warns as the count approaches the limit and says when new users are being refused. The refusal is recorded in the audit log as license.user_limit_reached and counted as searchcode_user_limit_refusals_total.

When the licence is absent, expired or invalid

Nothing here can stop the process. An absent, unreadable, tampered or expired licence, or one in a format this build does not recognise, puts the instance in community mode: it keeps serving with the community limits, logs a warning that says why, and reports the reason on /api/v1/status. The warning is logged at warn and not error, deliberately: every feature is working, and an error level would page somebody for a commercial event.

The expiry ladder

A licence with an expiry date moves through four states.

State When What changes
valid More than 30 days before expiry Nothing.
expiring Within 30 days of expiry Administrators see a notice. Other users see nothing.
grace Past expiry, inside the grace period the licence names Everything still works. The notice widens to every user.
past_grace Past the grace period Indexing pauses. Administrators are confined to the licence page in the admin UI.

Past grace, the list of things that keep working is the important half:

The estate goes stale rather than dark. A lapsed licence that widened access, or stopped recording security events, would be an incident at your site, and the ladder is shaped so that neither can happen.

Ingestion reports the pause as license_expired on searchcode_ingestion_paused, so the shipped alert rules see it.

A licence that covers older versions only

A licence can name the last product version it covers. Running a newer build than that is treated as past_grace and not as an invalid licence: the licence is genuine, it just does not cover this build. Indexing pauses and everything else keeps working, and the remedy is to renew or to run the last version the licence covers.

Clocks

A licence that is not valid yet is checked with a day of tolerance, because air-gapped machines often have no time synchronisation. If the system clock reads earlier than the binary's own build time, the instance warns that the clock is wrong, since a licence that appears "not yet valid" is then almost certainly the clock.

Checking the licence

On the command line, without a running instance:

searchcode --config /etc/searchcode/searchcode.yaml license show

It prints the licence's source, identity, issue and expiry dates, its state on the ladder, the reason for that state, and both meters with their current counts and limits. It works on a broken licence, which is the point: it renders what it could not verify and why.

On a running instance, the same report is the license block of /api/v1/status and the licence panel of the admin page. All three are computed in one place, so they cannot disagree.

In Prometheus, searchcode_license_expiry_timestamp_seconds, the two meters and their limits, and the refusal counter, with alert rules for expiry, refusals and the repository limit already written. See Monitoring.

The ladder state is logged at startup and re-evaluated daily, and a meter is logged when it crosses a level, so an operator who reads logs rather than the admin page still finds out.

Generated from the documentation that ships with searchcode enterprise. Questions: ben@boyter.org