Licensing & Feature Flags
Overview
Section titled “Overview”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.
Architecture
Section titled “Architecture”┌──────────────────────────────────────────────────────────────────┐│ 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)Request Flow
Section titled “Request Flow”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 itemsAPI routes: featureGuard(Feature.CREDIT_NOTES) → 403 if not licensedStorefront: calls /api/store/features → show/hide componentsComponents
Section titled “Components”1. Database Tables
Section titled “1. Database Tables”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).
2. License API (on Magic Access)
Section titled “2. License API (on Magic Access)”The Magic Access container (access.magicomniverse.online) exposes license management endpoints:
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/license/tenants | GET | No | List all tenants with feature counts |
/api/license/:tenant_id | GET | No | Get all features for a tenant (used by backends) |
/api/license/:tenant_id | PUT | Session | Toggle a single feature |
/api/license/:tenant_id/bulk | PUT | Session | Bulk toggle multiple features |
/api/license/:tenant_id/clone | POST | Session | Copy features from another tenant |
/api/license/:tenant_id/audit | GET | Session | Get 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.
3. Admin UI
Section titled “3. Admin UI”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
4. Backend Feature Cache
Section titled “4. Backend Feature Cache”Each tenant backend fetches features from the license API and maintains a local cache with disk snapshot for resilience.
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:
- License API response (primary, refreshed every 60s)
- Local snapshot file (
.cache/features-{tenant_id}.json) TENANT_FEATURESenv var (legacy bootstrap)- All-features mode (no data at all — backwards compatible for dev)
5. Admin Sidebar Integration
Section titled “5. Admin Sidebar Integration”The Medusa admin sidebar dynamically shows/hides menu items based on licensed features.
// On page load: fetch /admin/featuresconst { 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 hidden6. API Route Protection
Section titled “6. API Route Protection”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 handlersexport 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}7. Storefront Feature Checks
Section titled “7. Storefront Feature Checks”Public endpoint for storefront:
// GET /api/store/features{ "tenant_id": "brinxx", "features": ["quotations", "orders", "invoices", "cms", "..."]}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} />}Feature List
Section titled “Feature List”All 31 features that can be toggled per tenant:
| Feature | Group | Label (NL) | Sidebar Item |
|---|---|---|---|
quotations | Sales | Offertes | 01-quotations |
orders | Sales | Orders | 02-aplt-orders |
invoices | Sales | Facturen | 03-invoices |
credit_notes | Sales | Creditnota’s | 05-credit-notes |
payments | Sales | Betalingen | 06-payments |
customers | Sales | Klanten | 04-customers |
subscriptions | Sales | Abonnementen | 08-subscriptions |
reports | Sales | Rapporten | 07-reports |
dunning | Sales | Dunning | — |
products | Products | APLT Producten | 23-aplt-products |
technique_pricing | Products | Print Prijzen | 25-technique-pricing |
suppliers | Products | Leveranciers | 28-suppliers |
product_copy | Products | Artikel Kopiëren | 29-product-copy |
sales_channel_products | Products | SC Producten | 27-sales-channel-products |
cms | Content | CMS | 11-cms |
cms_modules | Content | CMS Modules | 13-cms-modules |
brand_wizard | Content | Brand Wizard | 12-brand-wizard |
page_manager | Content | Page Manager | 14-page-manager |
menu_manager | Content | Menu Manager | 15-menu-manager |
translations | Content | Vertalingen | 26-translations |
connectors | System | Connectors | 21-connectors |
access_requests | System | Toegangsverzoeken | 22-access-requests |
parameters | System | Parameters | 30-parameters |
crm | Add-ons | CRM | 09-crm |
designer_2d | Add-ons | 2D Designer | — |
designer_external | Add-ons | Externe Designer | — |
quotation_source_filter | Add-ons | Offerte Bronfilter | — |
purchase_orders | Add-ons | Inkooporders | — |
picking_lists | Add-ons | Picklists | — |
shipments | Add-ons | Verzendingen | — |
wayne_assist | Add-ons | Wayne Assist (AI) | — |
Tenant Feature Presets
Section titled “Tenant Feature Presets”Current recommended feature configurations per tenant:
| Tenant | Core Features | Add-ons |
|---|---|---|
| Brinxx | All sales, all products, all content, all system | technique_pricing, wayne_assist |
| Jodasign | All sales, products, CMS | quotation_source_filter |
| Logohorloge | All sales, products, CMS | designer_2d |
| Spranz | All sales, products, CMS | designer_external |
| Default | All sales, products, CMS | — |
| Demo | All features | All add-ons (demo purposes) |
| Desluis | Sales, products, CMS | — |
| Bovisales | Sales, products, CMS | — |
Environment Variables
Section titled “Environment Variables”| Variable | Where | Description |
|---|---|---|
TENANT_ID | Backend (Coolify) | Identifies which tenant this instance is — used to query the license API |
LICENSE_API_URL | Backend (Coolify) | License API base URL, default: http://magic-access:3334 |
TENANT_FEATURES | Backend (Coolify) | Legacy fallback — only used for initial bootstrap if no snapshot exists and API is unreachable |
PG_HOST | Magic Access | PostgreSQL host (magic-postgres) |
PG_DB | Magic Access | Database name (magic_access) |
PG_USER | Magic Access | Database user (postgres) |
PG_PASS | Magic Access | Database password |
Implementation Status
Section titled “Implementation Status”| Component | Status | Notes |
|---|---|---|
Feature flag definitions (backend/src/features/flags.ts) | ✅ Done | 31 features, 5 groups, sidebar mapping |
tenant_licenses database table | ✅ Done | Created on magic-postgres, Brinxx seeded |
tenant_license_audit audit table | ✅ Done | Created on magic-postgres |
| License API on Magic Access | ✅ Done | 6 endpoints, all working |
| Admin UI (toggle switches) | ✅ Done | At access.magicomniverse.online/admin/licenses |
Backend license API client (features/index.ts) | ✅ Done | Snapshot persistence, 60s polling, fallback chain |
| Admin sidebar filtering | ✅ Done | Dynamic show/hide based on features |
/admin/features API endpoint | ✅ Done | Returns features + sidebar items + status |
/api/store/features public endpoint | ✅ Done | Public storefront feature list |
Storefront hasFeature() helper | ✅ Done | Client-side feature checks |
featureGuard() on API routes | 🔲 Planned | Need to add to individual routes |
| Seed other tenants in database | 🔲 Planned | Currently only Brinxx is seeded |
| Cache invalidation webhook | 🔲 Planned | Push-based invalidation for instant updates |
| Role-based access for license changes | 🔲 Planned | Currently any authenticated user can change |
Security
Section titled “Security”- The license API
GETendpoints 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
Adding a New Tenant
Section titled “Adding a New Tenant”- Seed features in the database — Go to
access.magicomniverse.online/admin/licensesand use “Copy from tenant” to clone an existing tenant’s features, then customize - Set backend env vars — In Coolify, set
TENANT_IDand optionallyLICENSE_API_URLfor the new backend service - Deploy — The backend will fetch its features from the license API on startup
- Verify — Check
/admin/featureson the tenant’s admin panel to confirm features loaded correctly
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| All features visible (no filtering) | Backend in all-features-mode — no snapshot, no API response, no env var | Check TENANT_ID is set, verify magic-access container is running and on coolify network |
| Features not updating after toggle | 60s cache delay | Wait for next refresh cycle, or restart the backend |
| Admin UI shows 401 | Not logged in to Magic Access | Log in at access.magicomniverse.online first |
| License API returns empty tenants | No features seeded | Use admin UI to seed features or clone from another tenant |
| Backend logs “License API unreachable” | Network issue or Magic Access down | Check docker logs magic-access, verify container is on coolify network |