Skip to content

Licensing & Feature Flags

The licensing system controls which features are available per tenant. Features are stored in a central PostgreSQL database table (magic_access database) and managed via a toggle-switch admin UI on the Magic Access portal — no environment variables or redeployments needed to change a tenant’s feature set.


┌──────────────────────────────────────────────────────────────────┐
│ PostgreSQL (magic_access database — shared instance) │
│ │
│ tenant_licenses table │
│ ┌────────────┬──────────────────────┬─────────┬───────────┐ │
│ │ tenant_id │ feature │ enabled │ group │ │
│ ├────────────┼──────────────────────┼─────────┼───────────┤ │
│ │ brinxx │ quotations │ true │ sales │ │
│ │ brinxx │ orders │ true │ sales │ │
│ │ brinxx │ technique_pricing │ true │ products │ │
│ │ brinxx │ designer_2d │ false │ addons │ │
│ │ jodasign │ quotation_source_f… │ true │ addons │ │
│ │ logohorloge│ designer_2d │ true │ addons │ │
│ └────────────┴──────────────────────┴─────────┴───────────┘ │
│ │
│ tenant_license_audit table │
│ ┌────────────┬──────────┬─────┬─────┬───────────┬──────────┐ │
│ │ tenant_id │ feature │ old │ new │ actor │ reason │ │
│ └────────────┴──────────┴─────┴─────┴───────────┴──────────┘ │
└──────────────────────┬───────────────────────────────────────────┘
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
License API Admin UI Backend Cache
(Magic Access) (Toggle UI) (snapshot + 60s refresh)
Tenant backend starts
Load snapshot from disk (if exists) → immediate startup
GET http://magic-access:3334/api/license/{tenant_id}
Returns: { features: ['quotations','orders','invoices',...] }
Backend caches features in memory + writes snapshot to .cache/
├── Every 60 seconds: re-fetch from license API
├── On API failure: keep using last known snapshot
Admin sidebar: calls /admin/features → returns enabled sidebar items
API routes: featureGuard(Feature.CREDIT_NOTES) → 403 if not licensed
Storefront: calls /api/store/features → show/hide components

Stored in the magic_access database on the shared PostgreSQL instance (magic-postgres container).

CREATE TABLE tenant_licenses (
id SERIAL PRIMARY KEY,
tenant_id VARCHAR(50) NOT NULL,
feature VARCHAR(100) NOT NULL,
enabled BOOLEAN DEFAULT true,
feature_group VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id, feature)
);
CREATE INDEX idx_tenant_licenses_tenant ON tenant_licenses(tenant_id);
CREATE TABLE tenant_license_audit (
id SERIAL PRIMARY KEY,
tenant_id VARCHAR(50) NOT NULL,
feature VARCHAR(100) NOT NULL,
old_enabled BOOLEAN,
new_enabled BOOLEAN NOT NULL,
actor_email VARCHAR(255),
reason TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_tenant_license_audit_tenant ON tenant_license_audit(tenant_id);

Current status: Tables created and Brinxx seeded with 31 features (23 enabled, 8 disabled).

The Magic Access container (access.magicomniverse.online) exposes license management endpoints:

EndpointMethodAuthDescription
/api/license/tenantsGETNoList all tenants with feature counts
/api/license/:tenant_idGETNoGet all features for a tenant (used by backends)
/api/license/:tenant_idPUTSessionToggle a single feature
/api/license/:tenant_id/bulkPUTSessionBulk toggle multiple features
/api/license/:tenant_id/clonePOSTSessionCopy features from another tenant
/api/license/:tenant_id/auditGETSessionGet audit log (last 100 entries)

GET /api/license/brinxx response:

{
"tenant_id": "brinxx",
"features": ["quotations", "orders", "invoices", "customers", "cms", "technique_pricing", "..."],
"all_features": {
"quotations": true,
"orders": true,
"designer_2d": false,
"crm": false
},
"groups": {
"sales": { "enabled": ["quotations", "orders"], "disabled": ["dunning"] },
"products": { "enabled": ["technique_pricing"], "disabled": [] }
},
"feature_count": 23,
"total_count": 31
}

PUT /api/license/brinxx body:

{
"feature": "designer_2d",
"enabled": true,
"reason": "Enabled for Logohorloge rollout"
}

PUT /api/license/brinxx/bulk body:

{
"features": ["crm", "designer_2d", "shipments"],
"enabled": true,
"reason": "Q2 feature expansion"
}

POST /api/license/brinxx/clone body:

{
"source_tenant_id": "default"
}

Authorization: Write operations require a valid Magic Access session. Every write logs to tenant_license_audit with actor email, old/new value, and optional reason.

A toggle-switch admin page at https://access.magicomniverse.online/admin/licenses:

┌──────────────────────────────────────────────────────────┐
│ Tenant Feature Management ← Terug naar Portal│
├──────────────────────────────────────────────────────────┤
│ │
│ Tenant: [Brinxx (23/31)] [Default] [Demo] [Jodasign] │
│ │
│ [Alles Inschakelen] [Alles Uitschakelen] │
│ │
│ VERKOOP 8 / 9 actief │
│ ┌──────────────────────────────────┬──────────┐ │
│ │ Offertes quotations │ [●━━━] │ │
│ │ Orders orders │ [●━━━] │ │
│ │ Facturen invoices │ [●━━━] │ │
│ │ Creditnota's credit_notes │ [●━━━] │ │
│ │ Betalingen payments │ [●━━━] │ │
│ │ Klanten customers │ [●━━━] │ │
│ │ Abonnementen subscriptions │ [●━━━] │ │
│ │ Rapporten reports │ [●━━━] │ │
│ │ Dunning dunning │ [━━━○] │ │
│ └──────────────────────────────────┴──────────┘ │
│ │
│ ADD-ONS 1 / 8 actief │
│ ┌──────────────────────────────────┬──────────┐ │
│ │ Wayne Assist (AI) wayne_assist │ [●━━━] │ │
│ │ CRM crm │ [━━━○] │ │
│ │ 2D Designer designer_2d │ [━━━○] │ │
│ │ ... │ │ │
│ └──────────────────────────────────┴──────────┘ │
│ │
│ WIJZIGINGSLOG │
│ ┌──────────────────────────────────────────────┐ │
│ │ 23-03 10:15 dunning UIT admin@... - │ │
│ │ 23-03 10:14 wayne_a… AAN admin@... - │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

Key features:

  • Toggle switches update the database immediately
  • “Alles Inschakelen” / “Alles Uitschakelen” bulk toggles
  • Feature count per group shown in header
  • Tenant selector shows enabled/total counts
  • Audit log (last 20 entries) shown below features
  • Every change auto-logs with actor email

Each tenant backend fetches features from the license API and maintains a local cache with disk snapshot for resilience.

backend/src/features/index.ts
const LICENSE_API = process.env.LICENSE_API_URL || 'http://magic-access:3334'
const TENANT_ID = process.env.TENANT_ID || 'unknown'
// On startup:
// 1. Load snapshot from .cache/features-{tenant_id}.json (sync, instant)
// 2. Fetch from license API (async, updates cache + snapshot)
// 3. Every 60s: re-fetch from API
export function isFeatureEnabled(feature: Feature): boolean {
if (allFeaturesMode) return true // no license data → allow all (backwards compat)
return enabledFeatures.has(feature)
}

Fallback chain:

  1. License API response (primary, refreshed every 60s)
  2. Local snapshot file (.cache/features-{tenant_id}.json)
  3. TENANT_FEATURES env var (legacy bootstrap)
  4. All-features mode (no data at all — backwards compatible for dev)

The Medusa admin sidebar dynamically shows/hides menu items based on licensed features.

backend/src/admin/app.tsx
// On page load: fetch /admin/features
const { sidebar_items } = await res.json()
// sidebar_items = ['01-quotations', '02-aplt-orders', '03-invoices', ...]
// Menu items not in sidebar_items are hidden
// Section headers with no visible children are also hidden

Backend API routes use featureGuard() middleware to block requests for unlicensed features:

import { featureGuard, Feature } from '../../../features'
// In route handler:
// Medusa uses file-based routing, so featureGuard is called at the top of route handlers
export async function GET(req: MedusaRequest, res: MedusaResponse) {
// Guard check
if (!isFeatureEnabled(Feature.CREDIT_NOTES)) {
return res.status(403).json({ message: 'Feature not available', feature: 'credit_notes' })
}
// ... handle request
}

Public endpoint for storefront:

// GET /api/store/features
{
"tenant_id": "brinxx",
"features": ["quotations", "orders", "invoices", "cms", "..."]
}
storefront/src/lib/features.ts
const features = (process.env.NEXT_PUBLIC_TENANT_FEATURES || '').split(',').filter(Boolean)
const allFeaturesMode = features.length === 0
export function hasFeature(feature: string): boolean {
if (allFeaturesMode) return true
return features.includes(feature)
}
// Usage in components:
{hasFeature('designer_2d') && <Mini2DPreview product={product} />}

All 31 features that can be toggled per tenant:

FeatureGroupLabel (NL)Sidebar Item
quotationsSalesOffertes01-quotations
ordersSalesOrders02-aplt-orders
invoicesSalesFacturen03-invoices
credit_notesSalesCreditnota’s05-credit-notes
paymentsSalesBetalingen06-payments
customersSalesKlanten04-customers
subscriptionsSalesAbonnementen08-subscriptions
reportsSalesRapporten07-reports
dunningSalesDunning
productsProductsAPLT Producten23-aplt-products
technique_pricingProductsPrint Prijzen25-technique-pricing
suppliersProductsLeveranciers28-suppliers
product_copyProductsArtikel Kopiëren29-product-copy
sales_channel_productsProductsSC Producten27-sales-channel-products
cmsContentCMS11-cms
cms_modulesContentCMS Modules13-cms-modules
brand_wizardContentBrand Wizard12-brand-wizard
page_managerContentPage Manager14-page-manager
menu_managerContentMenu Manager15-menu-manager
translationsContentVertalingen26-translations
connectorsSystemConnectors21-connectors
access_requestsSystemToegangsverzoeken22-access-requests
parametersSystemParameters30-parameters
crmAdd-onsCRM09-crm
designer_2dAdd-ons2D Designer
designer_externalAdd-onsExterne Designer
quotation_source_filterAdd-onsOfferte Bronfilter
purchase_ordersAdd-onsInkooporders
picking_listsAdd-onsPicklists
shipmentsAdd-onsVerzendingen
wayne_assistAdd-onsWayne Assist (AI)

Current recommended feature configurations per tenant:

TenantCore FeaturesAdd-ons
BrinxxAll sales, all products, all content, all systemtechnique_pricing, wayne_assist
JodasignAll sales, products, CMSquotation_source_filter
LogohorlogeAll sales, products, CMSdesigner_2d
SpranzAll sales, products, CMSdesigner_external
DefaultAll sales, products, CMS
DemoAll featuresAll add-ons (demo purposes)
DesluisSales, products, CMS
BovisalesSales, products, CMS

VariableWhereDescription
TENANT_IDBackend (Coolify)Identifies which tenant this instance is — used to query the license API
LICENSE_API_URLBackend (Coolify)License API base URL, default: http://magic-access:3334
TENANT_FEATURESBackend (Coolify)Legacy fallback — only used for initial bootstrap if no snapshot exists and API is unreachable
PG_HOSTMagic AccessPostgreSQL host (magic-postgres)
PG_DBMagic AccessDatabase name (magic_access)
PG_USERMagic AccessDatabase user (postgres)
PG_PASSMagic AccessDatabase password

ComponentStatusNotes
Feature flag definitions (backend/src/features/flags.ts)✅ Done31 features, 5 groups, sidebar mapping
tenant_licenses database table✅ DoneCreated on magic-postgres, Brinxx seeded
tenant_license_audit audit table✅ DoneCreated on magic-postgres
License API on Magic Access✅ Done6 endpoints, all working
Admin UI (toggle switches)✅ DoneAt access.magicomniverse.online/admin/licenses
Backend license API client (features/index.ts)✅ DoneSnapshot persistence, 60s polling, fallback chain
Admin sidebar filtering✅ DoneDynamic show/hide based on features
/admin/features API endpoint✅ DoneReturns features + sidebar items + status
/api/store/features public endpoint✅ DonePublic storefront feature list
Storefront hasFeature() helper✅ DoneClient-side feature checks
featureGuard() on API routes🔲 PlannedNeed to add to individual routes
Seed other tenants in database🔲 PlannedCurrently only Brinxx is seeded
Cache invalidation webhook🔲 PlannedPush-based invalidation for instant updates
Role-based access for license changes🔲 PlannedCurrently any authenticated user can change

  • The license API GET endpoints are unauthenticated (used by backends on the Docker network)
  • All write endpoints (PUT, POST) require a valid Magic Access session
  • Every license change is audit logged with actor email, old/new value, and reason
  • Feature guards on API routes prevent access to unlicensed endpoints
  • Backends use last known snapshots during outages — never broaden access
  • The admin UI is behind Magic Access authentication

  1. Seed features in the database — Go to access.magicomniverse.online/admin/licenses and use “Copy from tenant” to clone an existing tenant’s features, then customize
  2. Set backend env vars — In Coolify, set TENANT_ID and optionally LICENSE_API_URL for the new backend service
  3. Deploy — The backend will fetch its features from the license API on startup
  4. Verify — Check /admin/features on the tenant’s admin panel to confirm features loaded correctly

SymptomCauseFix
All features visible (no filtering)Backend in all-features-mode — no snapshot, no API response, no env varCheck TENANT_ID is set, verify magic-access container is running and on coolify network
Features not updating after toggle60s cache delayWait for next refresh cycle, or restart the backend
Admin UI shows 401Not logged in to Magic AccessLog in at access.magicomniverse.online first
License API returns empty tenantsNo features seededUse admin UI to seed features or clone from another tenant
Backend logs “License API unreachable”Network issue or Magic Access downCheck docker logs magic-access, verify container is on coolify network