MXID ships as an open-core product. Community Edition (CE) is the default and fully usable on its own. Enterprise Edition (EE) unlocks additional features with a signed license.
| Capability | CE | EE |
|---|---|---|
| Password login, sessions, TOTP MFA | ✅ | ✅ |
| OIDC / SAML / CAS / JWT protocols | ✅ | ✅ |
| Users / orgs / groups, RBAC | ✅ | ✅ |
| SMTP email, basic audit | ✅ | ✅ |
| SMS OTP login | ✅ | ✅ |
| Step-up MFA (sudo mode for high-risk operations) | ✅ | ✅ |
| Single-tenant (one identity domain per deployment) | ✅ | ✅ |
| External IdP login (social / enterprise SSO) | ❌ | ✅ |
| Branding / white-label (logo, colors, login page) | ❌ | ✅ |
| Conditional access (risk signals: new country, impossible travel, unseen device — recorded and audited for local and federated sign-ins alike; not yet used to force a second factor) | ❌ | ✅ |
JIT privileged access (eligibility → request → approval → time-boxed elevation; gated via conditional_access) |
❌ | ✅ |
| SCIM outbound deprovisioning (offboarding L2 connector) | ❌ | ✅ |
| Form-fill SSO (SWA) — browser-extension auto-login for password-only web apps | ❌ | ✅ |
Feature keys sold today (every valid EE license grants exactly these —
see ImplementedFeatures in pkg/ee/license/features.go): branding,
conditional_access, external_idp, scim, form_fill.
Reserved catalog keys — accepted in a license payload for forward
compatibility but with no shipping code; they never grant anything:
webauthn, sms, advanced_stepup, multi_tenant. Two clarifications:
sms/advanced_stepupbeing reserved does not mean the capability is missing — SMS OTP login and step-up MFA ship in CE, ungated. The reserved keys exist only for a possible future advanced EE tier.multi_tenantis a permanently reserved key that is never granted: the product is single-tenant by design in both editions, and multi-tenancy is not a roadmap item.
- App template marketplace (CE): create apps from built-in onboarding presets (Feishu, DingTalk, WeCom, GitLab, Grafana, Jenkins, Jira, Confluence, JumpServer). Templates are declarative and contain no secrets; template-created apps run the same validation as hand-filled ones.
- SMS OTP login and step-up MFA (sudo mode for high-risk operations) —
both ship ungated in CE (the
sms/advanced_stepuplicense keys are reserved and unrelated to these). - Offboarding L1/L3: on user disable/delete, kill sessions and revoke
downstream SSO access (L1) plus notification/webhook fan-out (L3). The L2
SCIM deprovision connector is EE (
scim). - Transactional outbox for durable event delivery (offboarding domain).
- Tamper-proof audit chain: hash-chained audit log with Merkle anchoring,
plus third-party verifiable export (
export+verify). - Per-app provisioning config UI (offboarding actions per application).
- Batch access policies (bulk assignment of app access).
- Rate limiting (login and portal API, console-configurable).
- Dashboard and Prometheus
/metricsendpoint.
Three repositories, single source of truth, no fork:
github.com/imkerbos/mxid public CE product + app.Run() + pkg/ee/{license,registry}
github.com/imkerbos/mxid-ee private EE features; wraps app.Run(), garble-obfuscated
github.com/imkerbos/license-authority private per-product Ed25519 signing keys + issuance
Two layers of EE gating:
- Runtime-gated (
branding,conditional_access): the code lives in the CE binary — these features rely on foundational schema that CE already contains (and grandfathers on expiry).middleware.RequireFeature/license.Current().Has()returns 403 / locks the UI unless the license grants the feature. Expiry reverts to CE limits; existing data is grandfathered (see §Expiry below). - Code-separated (
external_idp,webauthn,scim,advanced_stepup,sms,form_fill, and other high-value features): the implementation lives ONLY in the privatemxid-eerepo. EE feature packages register intopkg/ee/registryfrom theirinit(); the CE binary imports none, so the code is physically absent from it — there is nothing to patch out. Verified: the CE binary contains zero EE symbols. The EE binary is built withgarble(symbol + control-flow obfuscation) as a further anti-tamper measure.
The license signature is the hard control: it is verified against an embedded
Ed25519 public key, so an operator cannot forge or edit a license. The
private signing key lives only in license-authority.
CE images are public on GHCR:
ghcr.io/imkerbos/mxid # backend
ghcr.io/imkerbos/mxid-web # nginx + SPAs (shared by both editions)
.env (see DEPLOYMENT.md) with MXID_TAG set, then
docker compose up -d. No license in the DB → CE.
The EE backend is a separate, private image (ghcr.io/imkerbos/mxid-ee,
garble-obfuscated). The web image is shared with CE. Select it via the EE
compose overlay in .env:
# external DB:
COMPOSE_FILE=deploy/compose/docker-compose.yml:deploy/compose/docker-compose.ee.yml
# self-contained (containerized Postgres + Redis):
# COMPOSE_FILE=deploy/compose/docker-compose.yml:deploy/compose/docker-compose.standalone.yml:deploy/compose/docker-compose.ee.yml
MXID_TAG=v1.8.0docker login ghcr.io # EE image is private — needs a read:packages token
docker compose pull
docker compose up -dThen activate the license in the console (see below). The EE image and the license are independent: the image provides the code-separated features; the license unlocks them at runtime.
CE image + EE license unlocks the runtime-gated features (
branding/conditional_access), but NOT the code-separated ones (external_idp,webauthn,scim, …) — those code paths do not exist in the CE binary at all; accessing them returns 404. EE customers runmxid-ee.
Switching CE → EE means pulling the
mxid-eeimage. The license is stored in the database and survives the image swap untouched — no re-activation needed.
A license is an Ed25519-signed token issued by license-authority:
# in the license-authority repo
go run ./cmd/sign -product mxid \
-customer "Acme Corp" -features all -exp 2027-01-01 -max-users 500Activate in the console — Settings → License: paste the token and save. It's verified, stored in the database, and the edition flips immediately (hot-reload, no restart). Being DB-persisted, it survives image swaps and restarts — pull a new image, it reads the same license. There is no env/file activation.
The console License page shows the resolved edition, customer, expiry, and unlocked features. Only the token is editable; everything else is derived from the verified signature.
GET /api/v1/system/info exposes edition + features. The features list
reflects only what the running binary actually contains: code-separated features
(external_idp, webauthn, scim, …) appear only when the EE binary is
running, because their registration happens in pkg/ee/registry at init() time
— if the code is absent, the feature is never registered and never listed.
Activating an EE license on the CE image does not cause external_idp to
appear in /system/info; it will still 404.
The frontend gates EE UI on these fields (e.g. the branding page is locked with an upsell banner in CE).
A license is portable by default — it runs on any installation. To stop one token being reused across many deployments, issue an install-bound license:
- Each install has a fingerprint =
HMAC(macKey, install_uuid | postgres system_identifier)— bound to the DB cluster, derived offline. It's shown on the console License page (Settings → License). - The operator gives that fingerprint to the vendor, who signs a bound token:
cmd/sign -install <fingerprint>. - A bound token only verifies on its installation; on any other it fails with
state
mismatchand the instance runs as CE. Copying the token + a DB dump to a different cluster changessystem_identifier, so the fingerprint no longer matches.
| Resource | CE | EE |
|---|---|---|
| Users | 100 (CEMaxUsers) |
unlimited (or the license's max_users) |
| Apps / orgs / groups / roles / API tokens | unlimited | unlimited |
| Audit retention | unlimited (default 365d, console-configurable) | unlimited |
| SMS OTP · step-up MFA | ✅ | ✅ |
| External IdP · branding · conditional access (incl. JIT privileged access) · SCIM deprovisioning · form-fill SSO | ❌ | ✅ |
| Control | Source | Enforced |
|---|---|---|
| Feature set | signed features[] |
route gates + UI + (code-separated) absence from the CE binary |
| Expiry | signed exp |
expired → reverts to CE limits (see below) |
| Product binding | signed product |
a license for another product is rejected |
| User cap | edition (license.Current()) |
create blocked at the cap; existing rows grandfathered |
An expired license does not delete anything or break logins. The instance reverts to CE limits, and existing data over those limits is grandfathered:
- Logins / SSO / token issuance keep working — unaffected.
- User count ≤ 100 → you can still create up to 100. Count already > 100 (grown under EE) → all existing users keep working, but no new users until renewal.
- External IdPs configured under EE keep functioning; you can't create new ones. Applied branding stays; you can't edit it.
Renew by pasting a fresh token in the console — the cap/features lift instantly.
Offline by design: no online revocation; bound risk with -exp + renewal.
See the license-authority repo: cmd/keygen (one key pair per product, public
half embedded in the product), cmd/sign (issue a token), products/<id>.yaml
(feature catalog), customers/ (issuance records). Private keys never leave the
authority repo / secrets manager.
Community Edition is AGPL-3.0 (LICENSE). The Enterprise Edition
(mxid-ee + EE-gated features) is governed by a commercial license
(LICENSE.EE). Copyright © 2026 MatrixPlus.