Backend Core Specification
Related documentation: Kisum System · Backend Core API contract · Backend Auth · Backend implementation · Backend modules · Data ownership · Infrastructure tasks · Error contract
Detailed Implementation Specification for Platform Core Backend
Section titled “Detailed Implementation Specification for Platform Core Backend”Audience: backend engineers, platform engineers, DevOps, architecture leads, platform-admin feature owners
Status: implementation specification
Scope: this document defines the Platform Core backend in full detail:
- what the Core service owns
- what it does not own
- PostgreSQL structure
- internal authentication
- request/response contracts
- how Auth calls it
- how Platform Admin writes to it
- how subscription/add-on/catalog changes are stored
- how entitlement versions are computed and invalidated
- how other services should consume it
- operational and security rules
Current implementation snapshot
Section titled “Current implementation snapshot”- Implemented now:
Backend-Kisum-Coreis an internal-only Chi service protected byX-Internal-API-KeymatchingCORE_INTERNAL_API_KEY. - Implemented now: the live router exposes health/readiness, internal catalog routes for modules/packages/add-ons, and internal company routes for company admin list, company master records, business-unit master records, profile, addresses, social links, documents, entitlements, subscription summary, history, Basic subscription upsert, and add-on upsert.
- Implemented now: Core now stores company master data directly in PostgreSQL through
companies,company_addresses,company_profiles,company_social_links, andcompany_documents. - Implemented now: existing entitlement-bearing company IDs are backfilled into
companiesduring migration, and the commercial tables now foreign-key to the Core company master table. - Implemented now: the generated OpenAPI in this docs site reflects those current internal routes and should be treated as the runtime contract for other backends.
- Implemented now: Core can send commercial lifecycle email via AWS SES only for Core-owned state changes such as company creation, company status changes, base subscription updates, and add-on updates. Email sending is asynchronous and best-effort.
- Still target-state: longer-term billing/provider automation, richer lifecycle orchestration, trials/renewals/grace periods, and external billing sync remain architectural direction rather than implemented runtime behavior.
- Docs rule for this page: sections below describe the intended Core ownership boundary, but any route, request, or response details must match the current
Backend-Kisum-Corerouter and OpenAPI.
0. Why this service exists
Section titled “0. Why this service exists”Platform Core exists to answer one question:
What does this company commercially own right now?
That means Platform Core is the commercial entitlement authority for:
- Basic subscription
- add-on modules
- package catalog
- add-on catalog
- module catalog
- package-to-module mappings
- add-on-to-module mappings
- company subscription state
- company add-on state
- entitlement versioning
Platform Core does not answer:
- who the user is
- what permissions the user has
- what tenant/company role the user has
- what a user can grant to another user
- whether a specific route is allowed for a specific user
Those belong to Auth.
1. Core service role in the full architecture
Section titled “1. Core service role in the full architecture”1.1 Final architecture rule
Section titled “1.1 Final architecture rule”Auth = identity + memberships + module grants + permissions + delegationCore = company master + commercial entitlementsBackends = business executionFrontend = UI consumer only1.2 Effective access rule
Section titled “1.2 Effective access rule”Auth computes final access as:
effective_access = company_entitlements ∩ membership_grantsCore only provides the left-hand side:
company_entitlements1.3 Main consumers of Platform Core
Section titled “1.3 Main consumers of Platform Core”Platform Core is consumed by:
- Auth Backend
- to resolve company entitlements during
/auth/me/access
- to resolve company entitlements during
- Platform Admin Backend
- to manage packages, add-ons, modules, company subscription state
- Optional internal jobs
- if later you introduce reconciliation, billing sync, catalog sync, or entitlement repair jobs
1.4 Platform Core is not public browser API
Section titled “1.4 Platform Core is not public browser API”Platform Core should be internal-only.
It should not be called directly by:
- browser frontend
- public clients
- mobile clients
Public flows should go through:
- Auth
- Platform Admin Backend
- business backends where appropriate
2. Service ownership
Section titled “2. Service ownership”2.1 What Platform Core owns
Section titled “2.1 What Platform Core owns”Platform Core owns the following domains.
Catalog
Section titled “Catalog”- modules
- packages
- add-ons
- mappings between packages/add-ons and modules
Company commercial state
Section titled “Company commercial state”- company master row
- company addresses
- company profile metadata
- company social links
- company documents
- company lifecycle state
- company creation source
- company billing/customer linkage metadata
- whether Basic subscription is active
- whether add-ons are active
- add-on start/end dates
- subscription start/end dates
- subscription status
- company entitlement version
- entitlement history / audit trail
- commercial lifecycle notifications related to Core-owned truth
Commercial email ownership
Section titled “Commercial email ownership”Core may send email only for events where Core is the commercial source of truth.
Current Core-owned email category:
- company created
- company status changed
- base subscription updated
- add-on updated
Core should not own:
- password reset email
- invitation email
- user activation email
- membership-change email
- login/security/session email
Those remain Auth-owned identity and access notifications.
Why the company sub-domains exist
Section titled “Why the company sub-domains exist”companies- the canonical Core company/commercial identity row
- stores the company id, lifecycle state, creation source, and billing/commercial linkage fields
business_units- the canonical Core business-unit master table
- stores business-unit identity, company linkage, code, slug, active state, and metadata
- does not store business-unit memberships
company_profiles- stores descriptive business metadata such as website, phone, timezone, industry, and description
- exists so descriptive data does not bloat the root company row
company_addresses- stores structured address records that may be legal, billing, mailing, or office addresses
- exists because a company may need more than one address
company_social_links- stores repeatable external/public links for the company
- exists because these are one-to-many normalized records, not a single profile field
company_documents- stores company-level document references and file metadata
- exists because document records have their own lifecycle and may point to external storage
Nested create/edit behavior in the current implementation
Section titled “Nested create/edit behavior in the current implementation”The current Backend-Kisum-Core implementation supports two ways to write company sub-domain data:
- direct sub-resource endpoints such as:
PUT /internal/companies/{companyId}/profilePOST /internal/companies/{companyId}/addressesPOST /internal/companies/{companyId}/social-linksPOST /internal/companies/{companyId}/documents
- nested company payloads sent to:
POST /internal/companiesPATCH /internal/companies/{companyId}
Current nested payload behavior:
profile- optional
- upserted into
company_profiles
addresses- optional
- when present on edit, Core synchronizes the full list against
company_addresses - rows with
idare updated - rows without
idare inserted - rows omitted from the submitted list are deleted
socialLinks- optional
- synchronized with the same list-replacement behavior against
company_social_links
documents- optional
- synchronized with the same list-replacement behavior against
company_documents
This means frontend can send the full company structure in one JSON payload during create or edit, while Core still preserves the normalized tables internally.
Safe PATCH rule
Section titled “Safe PATCH rule”For PATCH /internal/companies/{companyId}, the safest frontend/backend contract is:
- omit
addressesto leave addresses unchanged - omit
socialLinksto leave social links unchanged - omit
documentsto leave documents unchanged - send
[]only when the caller intentionally wants to clear that collection
This matches the current implementation:
- omitted collection keys decode as
niland are skipped - present empty arrays decode as non-nil empty slices and trigger full collection clear/sync
Therefore frontend should send only the sections it actually intends to modify.
Company admin list route in the current implementation
Section titled “Company admin list route in the current implementation”The current Core runtime also exposes:
GET /internal/companies
Purpose:
- paginated internal admin listing of companies
- returns a lightweight bundle per row so platform-admin tools can browse companies without calling every sub-resource separately
Current response contents per company row:
company- optional
profile addresses
Current response does not include:
socialLinksdocuments- entitlements
- subscription summary
- history
Current pagination behavior:
pagedefaults to1when omitted or invalidlimitdefaults to20when omitted or invalidlimitis capped to100
Business-unit master routes in the current implementation
Section titled “Business-unit master routes in the current implementation”The current Core runtime also exposes:
GET /internal/companies/{companyId}/business-unitsPOST /internal/companies/{companyId}/business-unitsPATCH /internal/business-units/{businessUnitId}
Purpose:
- manage Core-owned business-unit master data
- keep organizational structure in Core instead of Finance
- provide the canonical business-unit ids referenced by Auth memberships and module backends
Important ownership rule:
- Core owns business-unit master rows
- Auth owns business-unit memberships
So Core exposes business-unit master endpoints, but it does not expose business-unit membership endpoints.
Commercial domain extensions
Section titled “Commercial domain extensions”- Stripe product references
- Stripe price ids
- plan migrations
- trials
- renewals
- grace periods
- cancellations
- invoicing references
- self-service billing flags
2.2 What Platform Core explicitly does NOT own
Section titled “2.2 What Platform Core explicitly does NOT own”Not identity
Section titled “Not identity”- users
- passwords
- sessions
- JWT
- refresh token lifecycle
Not tenant-user access
Section titled “Not tenant-user access”- company memberships
- business unit memberships
- tenant roles
- membership module grants
- membership permissions
- delegation policies
- invitations
- invitation tokens
- invitation acceptance
- onboarding flows
- membership onboarding
Not business data
Section titled “Not business data”- events
- vendors
- venue business records
- AI outputs
- finance transactions
- market/touring documents
2.3 Source-of-truth rule
Section titled “2.3 Source-of-truth rule”Platform Core is the source of truth for:
- whether the company exists
- what lifecycle/commercial state the company is in
- whether the company has Basic
- which add-ons are active
- which modules are enabled by those products
Platform Core is not allowed to become a second source of truth for:
- user permissions
- user roles
- user module grants
That remains in Auth.
2.4 Invitations are not in Core
Section titled “2.4 Invitations are not in Core”Invitations do not belong to Platform Core.
Reason
Section titled “Reason”Invitations are part of:
- user onboarding
- identity lifecycle
- company membership creation
- tenant role assignment
Those responsibilities belong to Auth Backend.
Platform Core must NOT handle
Section titled “Platform Core must NOT handle”- invitation creation
- invitation storage
- invitation email sending
- invitation acceptance
- invitation token validation
- membership creation through invitation flow
Correct ownership
Section titled “Correct ownership”- Auth Backend → invitations, onboarding, membership creation
- Platform Core Backend → packages, add-ons, modules, subscriptions, entitlements
Important rule
Section titled “Important rule”If a company is invited, approved, or onboarded through invitation flow, the invitation logic still belongs to Auth.
Core only becomes relevant after that for commercial entitlement state.
Interaction with Core
Section titled “Interaction with Core”Invitations may result in a user joining a company that already has commercial entitlements.
However:
- Core does not participate in invitation flow
- Core does not validate invitations
- Core does not create memberships
After invitation acceptance:
- Auth creates membership
- Auth later calls Core (via
/auth/me/access) to resolve entitlements
Core remains unaware of invitation lifecycle.
3. Commercial model handled by Core
Section titled “3. Commercial model handled by Core”3.1 Products supported
Section titled “3.1 Products supported”A. Basic subscription
Section titled “A. Basic subscription”Commercial product that enables:
- Core App UI
- Basic module
- Basic business experience
B. Add-on modules
Section titled “B. Add-on modules”Commercial add-ons:
- finance
- artist
- vendor
- venue
- finance
- touring
- ai
- venue_marketplace
3.2 Important business rule
Section titled “3.2 Important business rule”A company can have add-ons without the promoter base module, as long as it still has a compatible base module active.
So these are all valid:
- promoter only
- artist + finance
- venue + ai
- promoter + venue_marketplace
- Basic + Finance + Market
- Venue + AI only
- no active Basic, but active add-ons
3.3 What Core must return
Section titled “3.3 What Core must return”Core must be able to return:
hasBasicbasePackage- active add-ons
- enabled modules
- entitlement version
4. Service location and deployment identity
Section titled “4. Service location and deployment identity”4.1 Recommended service hostname
Section titled “4.1 Recommended service hostname”core.kisum.ioOptional internal alias:
platform-core.kisum.io4.2 Recommended service name
Section titled “4.2 Recommended service name”Use one canonical service name in logs/metrics:
platform-core-backend4.3 Runtime assumptions
Section titled “4.3 Runtime assumptions”The service should:
- run as independent backend
- have its own PostgreSQL database
- be private/internal
- have health and readiness checks
- use structured logs
- use request IDs
- use strict config validation
5. Internal authentication model
Section titled “5. Internal authentication model”5.1 Who can call Platform Core
Section titled “5.1 Who can call Platform Core”Allowed callers
Section titled “Allowed callers”- Auth Backend
- Platform Admin Backend
- approved internal jobs/services
Not allowed
Section titled “Not allowed”- browser frontend
- public clients
- direct mobile apps
- arbitrary third-party clients
5.2 Required internal auth
Section titled “5.2 Required internal auth”Every internal route must require service-to-service authentication.
Allowed patterns:
- internal API key
- signed internal service JWT
- mTLS
- private network + internal header validation
Recommendation
Section titled “Recommendation”For first version:
- private network
- internal API key or signed internal service token
- clear allowlist of callers
Required headers example
Section titled “Required headers example”X-Internal-API-Key: <secret>Or, if using service JWT:
Authorization: Bearer <internal-service-token>5.3 Authorization model for callers
Section titled “5.3 Authorization model for callers”Auth Backend
Section titled “Auth Backend”Can call:
- entitlement reads
- catalog reads if needed
Platform Admin Backend
Section titled “Platform Admin Backend”Can call:
- catalog reads
- catalog writes
- company subscription writes
- company add-on writes
- company subscription summary reads
Internal jobs
Section titled “Internal jobs”Can call:
- read routes
- write routes only if explicitly allowed
6. PostgreSQL database structure
Section titled “6. PostgreSQL database structure”6.0 Mandatory physical schema (non-negotiable)
Section titled “6.0 Mandatory physical schema (non-negotiable)”A compliant Platform Core deployment MUST provision PostgreSQL with every object defined in this specification for Core:
- Section 7 — every
CREATE TABLEMUST be applied (all tables listed there MUST exist in the Core database). - Section 8 — every index MUST be applied.
- Section 9 — seed data MUST be loaded (or equivalent) so catalog rows exist as described.
Migrations are required: the service MUST NOT run against an empty or partial schema in production. “Optional” in this document refers to future tables (for example billing_products), not to skipping tables that are already specified with DDL.
Important company rule: Platform Core MUST define a full company structure. Company creation, lifecycle, profile/contact structure, and commercial identity are Core responsibilities. Everything else in sections 7–9 that has DDL MUST also exist—there is no optional subset of companies, company_addresses, company_profiles, company_social_links, company_documents, modules, packages, company_subscriptions, etc.
6.1 Database name
Section titled “6.1 Database name”platform_core_dbThis database must be separate from:
auth_db- finance DB
- market/touring DB
- main/basic DB
Same PostgreSQL cluster is acceptable, but separate database is preferred.
6.2 PostgreSQL extensions
Section titled “6.2 PostgreSQL extensions”Recommended:
pgcryptoif usinggen_random_uuid()- timezone support as standard
- JSONB support (native in Postgres)
6.3 Schema layout
Section titled “6.3 Schema layout”Recommended schema:
publicYou can later split into:
catalogentitlementsbilling
But first version can stay in public.
6.4 Company identity: companies table in Core
Section titled “6.4 Company identity: companies table in Core”Platform Core is the source of truth for the company master row.
This is mandatory because the platform already supports two creation paths:
- Platform Admin creates a company
- self-serve checkout creates a company before payment is finalized
Both paths must land in the same authoritative system.
Final rule
Section titled “Final rule”companieslives in Platform Corecompany_idremains the stable UUID shared across Auth, Core, Base, and module backends- Auth references
company_idfor memberships and access grants - Base and modules reference
company_idfor business/domain records
Why Core owns the company row
Section titled “Why Core owns the company row”The company row is not only an access concept. It is also not only profile data.
It is a tenant + commercial object that needs an authoritative place for:
- company existence
- addresses and contact structure
- profile/social/document metadata
- company lifecycle state
- creation source (
admin,self_serve,migration) - pending-payment state
- activation/suspension/archive state
- billing/customer linkage metadata
Required relational rule
Section titled “Required relational rule”The following Core tables must reference companies(id):
company_subscriptions.company_idcompany_addons.company_idcompany_entitlement_versions.company_identitlement_history.company_id
Naming vs informal docs
Section titled “Naming vs informal docs”- Prefer
company_subscriptions(not a generic table namesubscriptionsalone) - “Entitlements” in APIs means normalized response + computed
enabledModules+ version fields, not a requirement for one physical table namedentitlements
7. Core tables — exact structure
Section titled “7. Core tables — exact structure”Every subsection below includes normative DDL. Implementations MUST create these tables (and columns, constraints, and defaults) via migrations. The example starting with modules is required, not illustrative only.
7.0 companies
Section titled “7.0 companies”Purpose:
- tenant/company master row
- company lifecycle and commercial identity
- entrypoint for admin-created and self-serve-created companies
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists companies ( id uuid primary key default gen_random_uuid(), old_id text unique, name text not null, slug text unique, legal_name text, business_id text, email text, phone text, website text, status text not null check (status in ('draft', 'pending_payment', 'active', 'suspended', 'rejected', 'archived')), created_via text not null check (created_via in ('admin', 'self_serve', 'migration')), created_by_user_id uuid, billing_email text, stripe_customer_id text, country_iso2 text, is_active boolean not null default false, created_at timestamptz not null default now(), updated_at timestamptz not null default now());idis the canonicalcompany_idused platform-wideold_idstores legacy Mongo/company identifiers during migrationstatusis the authoritative lifecycle fieldis_activeshould remain consistent with lifecycle state- a company may exist before any active subscription exists
- self-serve checkout should create the company row first, typically with
status = 'pending_payment'
Required companion tables
Section titled “Required companion tables”The following tables are part of the mandatory Core company structure and MUST exist:
company_addressescompany_profilescompany_social_linkscompany_documents
7.0.1 company_addresses
Section titled “7.0.1 company_addresses”Purpose:
- normalized address storage for company identity and billing/legal use
- used by
POST /internal/companies/{companyId}/addresses - used when a company needs structured address records instead of free-text contact info
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_addresses ( id uuid primary key default gen_random_uuid(), company_id uuid not null references companies(id) on delete cascade, type text not null default 'primary' check (type in ('primary', 'billing', 'legal', 'office')), address1 text, address2 text, city text, region text, postal_code text, country text, country_iso2 text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());- every company address submitted by frontend must be stored in
company_addresses - first version may use one
primaryaddress only - schema allows billing/legal separation without redesign
- use this table for real address records, not for descriptive company profile text
7.0.2 company_profiles
Section titled “7.0.2 company_profiles”Purpose:
- store non-commercial company profile metadata separate from the master row
- used by
PUT /internal/companies/{companyId}/profile - used when the system needs descriptive business information that is not entitlement state or billing state
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_profiles ( company_id uuid primary key references companies(id) on delete cascade, logo_url text, references_agents text, references_artists text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());- one profile row per company
- optional record; a company can exist before profile data is filled in
- use this table for descriptive business fields, not subscription or access-control data
7.0.3 company_social_links
Section titled “7.0.3 company_social_links”Purpose:
- normalized company social link storage
- used by
POST /internal/companies/{companyId}/social-links - used when the company needs one or more external/public branded links
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_social_links ( id uuid primary key default gen_random_uuid(), company_id uuid not null references companies(id) on delete cascade, platform text not null, url text not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now());- one company can have many social links
- each row is one platform/url entry
- this table is for public/external links, not internal file references
7.0.4 company_documents
Section titled “7.0.4 company_documents”Purpose:
- store company-level files and reference documents
- used by
POST /internal/companies/{companyId}/documents - used when Core must register company-owned legal, compliance, or reference documents
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_documents ( id uuid primary key default gen_random_uuid(), company_id uuid not null references companies(id) on delete cascade, name text not null, file_type text, url text not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now());- one company can have many documents
- document rows may point to external object storage via
urlor future storage-key metadata - this table is for company-owned reference documents, not arbitrary business-domain entities unless Core is the owner
7.1 modules
Section titled “7.1 modules”Purpose:
- canonical list of modules the platform understands
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists modules ( id uuid primary key default gen_random_uuid(), key text not null unique, name text not null, type text not null check (type in ('base', 'addon')), description text, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now());key values:
promoterartistvendorvenuefinancetouringaivenue_marketplace
type:
base→ module delivered by Basic subscriptionaddon→ module delivered by add-on
7.2 packages
Section titled “7.2 packages”Purpose:
- commercial package catalog
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists packages ( id uuid primary key default gen_random_uuid(), key text not null unique, name text not null, description text, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now());Initially expected:
basic
Future expansion possible:
basic_plusenterprise- etc.
7.3 addons
Section titled “7.3 addons”Purpose:
- commercial add-on catalog
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists addons ( id uuid primary key default gen_random_uuid(), key text not null unique, name text not null, description text, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now());Expected rows
Section titled “Expected rows”- finance
- market
- touring
- venue
- ai
7.4 package_modules
Section titled “7.4 package_modules”Purpose:
- maps package → modules
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists package_modules ( package_id uuid not null references packages(id) on delete cascade, module_id uuid not null references modules(id) on delete cascade, created_at timestamptz not null default now(), primary key (package_id, module_id));Initially:
- package
basic→ modulebasic
7.5 addon_modules
Section titled “7.5 addon_modules”Purpose:
- maps addon → modules
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists addon_modules ( addon_id uuid not null references addons(id) on delete cascade, module_id uuid not null references modules(id) on delete cascade, created_at timestamptz not null default now(), primary key (addon_id, module_id));Initially:
- addon
finance→ modulefinance - addon
touring→ moduletouring - addon
ai→ moduleai - addon
venue_marketplace→ modulevenue_marketplace
7.6 company_subscriptions
Section titled “7.6 company_subscriptions”Purpose:
- stores company Basic subscription state
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_subscriptions ( id uuid primary key default gen_random_uuid(), company_id uuid not null references companies(id) on delete cascade, package_id uuid not null references packages(id), status text not null check (status in ('active', 'inactive', 'cancelled', 'expired', 'trial', 'paused')), starts_at timestamptz, ends_at timestamptz, source text, external_reference text, entitlement_version integer not null default 1, created_by text, updated_by text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (company_id, package_id));company_idreferencescompanies.id- one company may have 0 or 1 active Basic package in first version
- if future multi-package model is needed, schema already supports it
sourceexamples:platform_adminself_servicebilling_sync
external_referencemay store Stripe subscription id or invoice ref later
7.7 company_addons
Section titled “7.7 company_addons”Purpose:
- stores company add-on state
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_addons ( id uuid primary key default gen_random_uuid(), company_id uuid not null references companies(id) on delete cascade, addon_id uuid not null references addons(id), status text not null check (status in ('active', 'inactive', 'cancelled', 'expired', 'trial', 'paused')), starts_at timestamptz, ends_at timestamptz, source text, external_reference text, created_by text, updated_by text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (company_id, addon_id));Supports:
- one company with many add-ons
- add-ons active on different compatible base modules, not just
promoter
7.8 company_entitlement_versions
Section titled “7.8 company_entitlement_versions”Purpose:
- explicit version tracker for entitlement invalidation
This table is mandatory.
company_subscriptions.entitlement_version alone is not sufficient for a clean cross-row invalidation model.
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists company_entitlement_versions ( company_id uuid primary key references companies(id) on delete cascade, entitlement_version integer not null default 1, updated_at timestamptz not null default now(), updated_by text);Why required
Section titled “Why required”Because any of the following must bump the version:
- Basic activated/deactivated
- add-on activated/deactivated
- catalog mapping changes that affect a company’s enabled modules
- manual repair of entitlement state
Using a dedicated table keeps invalidation and response generation consistent.
7.9 entitlement_history
Section titled “7.9 entitlement_history”Purpose:
- audit trail of entitlement changes
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists entitlement_history ( id uuid primary key default gen_random_uuid(), company_id uuid not null references companies(id) on delete cascade, change_type text not null, entity_type text not null, entity_key text, previous_status text, new_status text, payload_json jsonb not null default '{}'::jsonb, source text, changed_by text, created_at timestamptz not null default now());Example values
Section titled “Example values”change_type:
basic_activatedbasic_deactivatedaddon_activatedaddon_deactivatedcatalog_updatedentitlement_repaired
entity_type:
packageaddonmappingcompany
7.10 billing_products
Section titled “7.10 billing_products”Purpose:
- hold external billing references
- provide a normalized place for Stripe/provider product and price mapping
SQL (MUST be created)
Section titled “SQL (MUST be created)”create table if not exists billing_products ( id uuid primary key default gen_random_uuid(), entity_type text not null check (entity_type in ('package', 'addon')), entity_id uuid not null, provider text not null, provider_product_id text, provider_price_id text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());- this table exists even if Stripe integration is not active yet
- provider reference columns may remain null until billing integration is enabled
8. Required indexes
Section titled “8. Required indexes”These index definitions MUST be applied (same migration discipline as section 7). A deployment that lacks them is not a complete Core schema.
create index if not exists idx_modules_key on modules(key);create index if not exists idx_packages_key on packages(key);create index if not exists idx_addons_key on addons(key);
create index if not exists idx_package_modules_package_id on package_modules(package_id);create index if not exists idx_package_modules_module_id on package_modules(module_id);
create index if not exists idx_addon_modules_addon_id on addon_modules(addon_id);create index if not exists idx_addon_modules_module_id on addon_modules(module_id);
create index if not exists idx_company_subscriptions_company_id on company_subscriptions(company_id);create index if not exists idx_company_subscriptions_status on company_subscriptions(status);
create index if not exists idx_company_addons_company_id on company_addons(company_id);create index if not exists idx_company_addons_status on company_addons(status);
create index if not exists idx_entitlement_history_company_id on entitlement_history(company_id);create index if not exists idx_entitlement_history_created_at on entitlement_history(created_at);9. Seed data to insert
Section titled “9. Seed data to insert”The inserts below MUST be executed (or produce the same catalog rows) so module/package/add-on keys exist before serving traffic.
9.1 Modules
Section titled “9.1 Modules”insert into modules (key, name, type, description) values('basic', 'Core App', 'base', 'Core App / Basic product module'),('finance', 'Finance', 'addon', 'Finance module'),('market', 'Market', 'addon', 'Market module'),('touring', 'Touring', 'addon', 'Touring module'),('venue', 'Venue', 'addon', 'Venue module'),('ai', 'AI', 'addon', 'AI module')on conflict (key) do nothing;9.2 Packages
Section titled “9.2 Packages”insert into packages (key, name, description) values('basic', 'Basic', 'Basic subscription that enables Core App')on conflict (key) do nothing;9.3 Add-ons
Section titled “9.3 Add-ons”insert into addons (key, name, description) values('finance', 'Finance', 'Finance add-on'),('market', 'Market', 'Market add-on'),('touring', 'Touring', 'Touring add-on'),('venue', 'Venue', 'Venue add-on'),('ai', 'AI', 'AI add-on')on conflict (key) do nothing;9.4 Package mappings
Section titled “9.4 Package mappings”insert into package_modules (package_id, module_id)select p.id, m.idfrom packages pjoin modules m on m.key = 'basic'where p.key = 'basic'on conflict do nothing;9.5 Add-on mappings
Section titled “9.5 Add-on mappings”insert into addon_modules (addon_id, module_id)select a.id, m.idfrom addons ajoin modules m on m.key = a.keyon conflict do nothing;10. Core data model rules
Section titled “10. Core data model rules”10.1 Company can have a base module without add-ons
Section titled “10.1 Company can have a base module without add-ons”Valid.
10.2 Company can have add-ons without the promoter base module
Section titled “10.2 Company can have add-ons without the promoter base module”Valid.
10.3 Company can have a base module plus add-ons
Section titled “10.3 Company can have a base module plus add-ons”Valid.
10.4 Company may have no active promoter base module and still have active add-ons
Section titled “10.4 Company may have no active promoter base module and still have active add-ons”Valid and must be supported everywhere.
10.5 Enabled modules algorithm
Section titled “10.5 Enabled modules algorithm”Core computes:
enabledModules = package modules from active Basic subscription + add-on modules from active company add-ons10.6 Example
Section titled “10.6 Example”Example A
Section titled “Example A”- Basic inactive
- Add-ons active: finance, touring
Result:
{ "hasBasic": false, "enabledModules": ["finance", "market"]}Example B
Section titled “Example B”- Basic active
- Add-ons active: finance
Result:
{ "hasBasic": true, "enabledModules": ["basic", "finance"]}11. Endpoints — full list
Section titled “11. Endpoints — full list”There are 3 groups of endpoints:
- Catalog read
- Entitlement read
- Catalog/company write
- Admin/audit/support read
All are internal-only.
Important restriction
Section titled “Important restriction”Platform Core must not expose any endpoints related to:
- invitations
- onboarding
- user creation
- membership creation
If such endpoints are required, they must be implemented in Auth.
12. Response envelope standard
Section titled “12. Response envelope standard”12.1 Success
Section titled “12.1 Success”{ "success": true, "data": {}}12.2 Error
Section titled “12.2 Error”{ "success": false, "error": { "code": "string_code", "message": "Human readable message" }}12.3 Common error codes
Section titled “12.3 Common error codes”unauthorizedforbiddenvalidation_errornot_foundconflictinternal_errorservice_unavailable
13. Health and discovery endpoints
Section titled “13. Health and discovery endpoints”13.1 GET /health
Section titled “13.1 GET /health”Purpose:
- simple liveness probe
Response
Section titled “Response”{ "success": true, "data": { "status": "ok" }}13.2 GET /ready
Section titled “13.2 GET /ready”Purpose:
- readiness check
Must verify:
- Core DB reachable
- internal config loaded
Failure response
Section titled “Failure response”{ "success": false, "error": { "code": "not_ready", "message": "core database unavailable" }}14. Catalog read endpoints
Section titled “14. Catalog read endpoints”14.1 GET /internal/catalog/modules
Section titled “14.1 GET /internal/catalog/modules”Who can call
Section titled “Who can call”- Platform Admin Backend
- Auth Backend (optional)
- internal jobs
Purpose
Section titled “Purpose”Return full module catalog.
Response
Section titled “Response”{ "success": true, "data": { "modules": [ { "id": "uuid", "key": "basic", "name": "Core App", "type": "base", "description": "Core App / Basic product module", "isActive": true }, { "id": "uuid", "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true } ] }}14.2 GET /internal/catalog/packages
Section titled “14.2 GET /internal/catalog/packages”Who can call
Section titled “Who can call”- Platform Admin Backend
- internal jobs
Response
Section titled “Response”{ "success": true, "data": { "packages": [ { "id": "uuid", "key": "basic", "name": "Basic", "description": "Basic subscription that enables Core App", "isActive": true, "modules": ["basic"] } ] }}14.3 GET /internal/catalog/addons
Section titled “14.3 GET /internal/catalog/addons”Who can call
Section titled “Who can call”- Platform Admin Backend
- internal jobs
Response
Section titled “Response”{ "success": true, "data": { "addons": [ { "id": "uuid", "key": "finance", "name": "Finance", "description": "Finance add-on", "isActive": true, "modules": ["finance"] }, { "id": "uuid", "key": "market", "name": "Market", "description": "Market add-on", "isActive": true, "modules": ["market"] } ] }}15. Entitlement read endpoints
Section titled “15. Entitlement read endpoints”15.1 GET /internal/companies/{companyId}/entitlements
Section titled “15.1 GET /internal/companies/{companyId}/entitlements”Who can call
Section titled “Who can call”- Auth Backend (main consumer)
- Platform Admin Backend
- internal jobs
Purpose
Section titled “Purpose”Return the current commercial entitlement state for a company.
Path params
Section titled “Path params”companyId: canonical UUID
Response
Section titled “Response”{ "success": true, "data": { "companyId": "cmp_001", "hasBasic": false, "basePackage": null, "addons": [ { "key": "finance", "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z" }, { "key": "market", "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z" } ], "enabledModules": ["finance", "market"], "entitlementVersion": 7, "updatedAt": "2026-04-16T05:00:00Z" }}- if Basic inactive,
hasBasic = false basePackagemay benullenabledModulesstill may include add-ons- output must always be normalized
15.2 GET /internal/companies/{companyId}/subscription-summary
Section titled “15.2 GET /internal/companies/{companyId}/subscription-summary”Who can call
Section titled “Who can call”- Platform Admin Backend
- internal jobs
- Auth Backend (optional)
Purpose
Section titled “Purpose”Admin-facing summary of commercial state.
Response
Section titled “Response”{ "success": true, "data": { "companyId": "cmp_001", "hasBasic": true, "basePackage": "basic", "addons": ["finance", "market"], "status": "active", "entitlementVersion": 7 }}15.3 GET /internal/companies/{companyId}/history
Section titled “15.3 GET /internal/companies/{companyId}/history”Who can call
Section titled “Who can call”- Platform Admin Backend
- internal audit/reporting jobs
Purpose
Section titled “Purpose”Return entitlement history.
Response
Section titled “Response”{ "success": true, "data": { "companyId": "cmp_001", "history": [ { "id": "uuid", "changeType": "addon_activated", "entityType": "addon", "entityKey": "finance", "previousStatus": "inactive", "newStatus": "active", "source": "platform_admin", "changedBy": "admin_user_uuid", "createdAt": "2026-04-16T05:00:00Z" } ] }}16. Catalog write endpoints
Section titled “16. Catalog write endpoints”These are mainly consumed through the Platform Admin Backend.
16.1 POST /internal/catalog/modules
Section titled “16.1 POST /internal/catalog/modules”Who can call
Section titled “Who can call”- Platform Admin Backend only
Request
Section titled “Request”{ "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true}Response
Section titled “Response”{ "success": true, "data": { "id": "uuid", "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true }}Validation
Section titled “Validation”keyuniquetypemust bebaseoraddon
16.2 PATCH /internal/catalog/modules/{moduleId}
Section titled “16.2 PATCH /internal/catalog/modules/{moduleId}”Request
Section titled “Request”{ "name": "Finance", "description": "Finance module updated", "isActive": true}keyshould generally be immutable once used in production
16.3 POST /internal/catalog/packages
Section titled “16.3 POST /internal/catalog/packages”Request
Section titled “Request”{ "key": "basic", "name": "Basic", "description": "Basic subscription that enables Core App", "isActive": true, "moduleKeys": ["basic"]}Behavior
Section titled “Behavior”- create package row
- map module keys in
package_modules
16.4 PATCH /internal/catalog/packages/{packageId}
Section titled “16.4 PATCH /internal/catalog/packages/{packageId}”Request
Section titled “Request”{ "name": "Basic", "description": "Updated Basic description", "isActive": true, "moduleKeys": ["basic"]}Behavior
Section titled “Behavior”- update package
- replace mapping set if
moduleKeyspresent - if mapping change impacts active companies, entitlement version repair may be needed
16.5 POST /internal/catalog/addons
Section titled “16.5 POST /internal/catalog/addons”Request
Section titled “Request”{ "key": "finance", "name": "Finance", "description": "Finance add-on", "isActive": true, "moduleKeys": ["finance"]}16.6 PATCH /internal/catalog/addons/{addonId}
Section titled “16.6 PATCH /internal/catalog/addons/{addonId}”Request
Section titled “Request”{ "name": "Finance", "description": "Finance updated", "isActive": true, "moduleKeys": ["finance"]}17. Company entitlement write endpoints
Section titled “17. Company entitlement write endpoints”These are the most important write endpoints.
17.1 POST /internal/companies/{companyId}/basic
Section titled “17.1 POST /internal/companies/{companyId}/basic”Who can call
Section titled “Who can call”- Platform Admin Backend
- optional internal billing/reconciliation jobs
Purpose
Section titled “Purpose”Activate, update, pause, or deactivate Basic subscription.
Request
Section titled “Request”{ "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z", "source": "platform_admin", "externalReference": "sub_123"}Behavior
Section titled “Behavior”- find package
basic - upsert company subscription row
- update status/dates/source/external ref
- bump entitlement version
- write entitlement history
Response
Section titled “Response”{ "success": true, "data": { "companyId": "cmp_001", "hasBasic": true, "basePackage": "basic", "entitlementVersion": 8 }}Valid statuses
Section titled “Valid statuses”activeinactivecancelledexpiredtrialpaused
17.2 POST /internal/companies/{companyId}/addons
Section titled “17.2 POST /internal/companies/{companyId}/addons”Who can call
Section titled “Who can call”- Platform Admin Backend
- optional internal billing jobs
Purpose
Section titled “Purpose”Activate or update one add-on for a company.
Request
Section titled “Request”{ "addonKey": "finance", "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z", "source": "platform_admin", "externalReference": "addon_sub_123"}Behavior
Section titled “Behavior”- resolve addon by
addonKey - upsert
company_addons - bump entitlement version
- write history
Response
Section titled “Response”{ "success": true, "data": { "companyId": "cmp_001", "addonKey": "finance", "status": "active", "entitlementVersion": 9 }}17.3 POST /internal/companies/{companyId}/deactivate-addon
Section titled “17.3 POST /internal/companies/{companyId}/deactivate-addon”Optional alternative
Section titled “Optional alternative”You may choose not to create a separate endpoint and instead use the same POST /addons route with status: inactive.
That is actually preferred for simplicity.
Recommendation
Section titled “Recommendation”Use:
POST /internal/companies/{companyId}/addonsfor both activate and deactivate.
18. Versioning and cache invalidation
Section titled “18. Versioning and cache invalidation”18.1 Why entitlement version exists
Section titled “18.1 Why entitlement version exists”Auth caches merged access.
When Core changes company entitlements, Auth must know access is stale.
So Core must bump:
entitlementVersionon every meaningful entitlement change.
18.2 Events that MUST bump entitlement version
Section titled “18.2 Events that MUST bump entitlement version”- Basic activated
- Basic deactivated
- Basic expired
- add-on activated
- add-on deactivated
- add-on expired
- package mapping changed in a way that affects active companies
- add-on mapping changed in a way that affects active companies
- manual entitlement repair
18.3 How to store version
Section titled “18.3 How to store version”Preferred:
company_entitlement_versionstable
Fallback:
- version stored on
company_subscriptions
Preferred is cleaner.
18.4 How Auth consumes it
Section titled “18.4 How Auth consumes it”Auth receives:
entitlementVersionfrom Core response and uses it in cache key or stale-check logic.
Example cache key:
access:{companyId}:{membershipId}:{accessVersion}:{entitlementVersion}19. How Auth calls Core
Section titled “19. How Auth calls Core”19.1 Main runtime call
Section titled “19.1 Main runtime call”Auth calls:
GET /internal/companies/{companyId}/entitlementsAuth needs from Core:
Section titled “Auth needs from Core:”hasBasicbasePackageaddonsenabledModulesentitlementVersion
Auth then merges with:
Section titled “Auth then merges with:”- membership grants
- permissions
- delegation
Core does not perform the merge.
19.2 Failure behavior
Section titled “19.2 Failure behavior”If Core is unavailable:
- Auth should fail
/auth/me/access - return 503
- do not guess entitlements
- do not fallback to stale business assumptions
20. How Platform Admin writes to Core
Section titled “20. How Platform Admin writes to Core”20.1 General rule
Section titled “20.1 General rule”Frontend admin UI should not call Core directly.
It calls:
- Platform Admin Backend
Platform Admin Backend then calls Core internal routes.
20.2 Catalog creation flow
Section titled “20.2 Catalog creation flow”Example: create addon
Section titled “Example: create addon”- Platform Admin UI → Admin Backend
- Admin Backend validates platform-admin access
- Admin Backend calls:
POST /internal/catalog/addons
- Core writes
addons - Core returns created addon
20.3 Company activation flow
Section titled “20.3 Company activation flow”Example: activate Basic
Section titled “Example: activate Basic”- Platform Admin UI → Admin Backend
- Admin Backend validates admin permissions
- Admin Backend calls:
POST /internal/companies/{companyId}/basic
- Core upserts subscription
- Core bumps entitlement version
- Core returns updated state
- Auth access cache must become stale
20.4 Add-on activation flow
Section titled “20.4 Add-on activation flow”Example: activate Finance addon
Section titled “Example: activate Finance addon”- Platform Admin UI → Admin Backend
- Admin Backend validates platform admin
- Admin Backend calls:
POST /internal/companies/{companyId}/addons
- Core upserts addon status
- Core bumps entitlement version
- Core writes history
- Auth invalidates / rebuilds access next time
21. Request validation rules
Section titled “21. Request validation rules”21.1 UUID rules
Section titled “21.1 UUID rules”companyId,moduleId,packageId,addonIdmust be valid UUIDs where path expects UUID
21.2 Key rules
Section titled “21.2 Key rules”module.key,package.key,addon.keyshould be lowercase slug format- no spaces
- immutable after production use if possible
21.3 Status rules
Section titled “21.3 Status rules”Allowed status values:
activeinactivecancelledexpiredtrialpaused
21.4 Date rules
Section titled “21.4 Date rules”startsAt <= endsAtif both present- dates optional for manually controlled states
- server should normalize timezone to UTC
22. Error model
Section titled “22. Error model”22.1 Example validation error
Section titled “22.1 Example validation error”{ "success": false, "error": { "code": "validation_error", "message": "addonKey is required" }}22.2 Example conflict
Section titled “22.2 Example conflict”{ "success": false, "error": { "code": "conflict", "message": "addon key already exists" }}22.3 Example unauthorized
Section titled “22.3 Example unauthorized”{ "success": false, "error": { "code": "unauthorized", "message": "missing or invalid internal credentials" }}22.4 Example not found
Section titled “22.4 Example not found”{ "success": false, "error": { "code": "not_found", "message": "company not found" }}23. Service structure recommendation
Section titled “23. Service structure recommendation”Recommended Go project structure:
/backend-kisum-core /cmd/api /internal/config /internal/http /handlers /middleware /internal/catalog /internal/entitlements /internal/history /internal/repository /postgres /internal/types /internal/errors /migrations23.1 Responsibilities by package
Section titled “23.1 Responsibilities by package”/internal/catalog
Section titled “/internal/catalog”- modules CRUD
- packages CRUD
- addons CRUD
- package/addon mappings
/internal/entitlements
Section titled “/internal/entitlements”- company Basic state
- company add-on state
- enabled modules computation
- entitlement version bumping
/internal/history
Section titled “/internal/history”- write/read entitlement history
/internal/repository/postgres
Section titled “/internal/repository/postgres”- all SQL access
24. Operational requirements
Section titled “24. Operational requirements”24.1 Logging
Section titled “24.1 Logging”Must log:
- entitlement reads
- catalog writes
- company subscription writes
- add-on writes
- entitlement version bumps
- invalid internal auth attempts
24.2 Metrics
Section titled “24.2 Metrics”Track:
- entitlement lookup count
- entitlement lookup latency
- catalog mutation count
- company mutation count
- 4xx/5xx rate
- history insert failures
24.3 Readiness
Section titled “24.3 Readiness”/ready must verify:
- DB connectivity
- config loaded
24.4 Backup
Section titled “24.4 Backup”Core DB must be backed up independently.
25. Security requirements
Section titled “25. Security requirements”- Core must not be public browser API
- internal routes must require service auth
- DB credentials must be unique to Core
- no cross-service DB writes from Auth to Core DB
- no user permissions stored in Core
- no JWT generation in Core
- no tenant role interpretation in Core
26. What Auth must NOT expect from Core
Section titled “26. What Auth must NOT expect from Core”Auth must not expect Core to return:
- permissions
- delegation
- tenant roles
- user grants
- membership objects
Core returns only:
- company commercial state
- enabled modules
- entitlement version
- product catalog state if asked
27. What Platform Core must NOT do
Section titled “27. What Platform Core must NOT do”- do not read Auth DB directly
- do not validate frontend user JWTs as if it were public API
- do not compute user-level access
- do not duplicate user grants
- do not become tenant-authorization service
- do not manage invitations or onboarding flows
28. Minimum implementation checklist
Section titled “28. Minimum implementation checklist”Database
Section titled “Database”- create
platform_core_db - create all tables
- create indexes
- seed catalog
Service
Section titled “Service”- bootstrap service
- add health/ready
- add internal auth middleware
- add structured logging
Catalog APIs
Section titled “Catalog APIs”-
GET /internal/catalog/modules -
GET /internal/catalog/packages -
GET /internal/catalog/addons -
POST /internal/catalog/modules -
PATCH /internal/catalog/modules/{id} -
POST /internal/catalog/packages -
PATCH /internal/catalog/packages/{id} -
POST /internal/catalog/addons -
PATCH /internal/catalog/addons/{id}
Entitlement APIs
Section titled “Entitlement APIs”-
GET /internal/companies/{companyId}/entitlements -
GET /internal/companies/{companyId}/subscription-summary -
GET /internal/companies/{companyId}/history -
POST /internal/companies/{companyId}/basic -
POST /internal/companies/{companyId}/addons
Invalidation
Section titled “Invalidation”- entitlement version table or equivalent
- version bump logic
- history writing
- invalidation strategy documented
29. Final summary
Section titled “29. Final summary”Platform Core is the commercial entitlement backend.It owns packages, add-ons, modules, and company product state.It does not own users, permissions, or membership access.Auth calls Core to get company entitlements.Platform Admin writes to Core to change commercial state.Core returns normalized enabled modules and entitlementVersion.30. Access model layers (REFERENCE — DO NOT IMPLEMENT HERE)
Section titled “30. Access model layers (REFERENCE — DO NOT IMPLEMENT HERE)”Platform Core participates in a 3-layer access model but only owns Level 1.
Level 1 — Company entitlements (OWNED BY CORE)
Section titled “Level 1 — Company entitlements (OWNED BY CORE)”Defines what the company has purchased:
- Basic
- Finance
- Market
- Touring
- Venue
- AI
Source of truth: Platform Core DB
Level 2 — Membership module grants (OWNED BY AUTH)
Section titled “Level 2 — Membership module grants (OWNED BY AUTH)”Defines which modules a user can access.
Core MUST NOT store or evaluate this.
Level 3 — Permissions (OWNED BY AUTH)
Section titled “Level 3 — Permissions (OWNED BY AUTH)”Defines actions inside modules.
Core MUST NOT store or evaluate this.
Critical rule
Section titled “Critical rule”Core returns entitlements ONLYCore never computes user access31. Relationship with Auth (STRICT)
Section titled “31. Relationship with Auth (STRICT)”Platform Core is a dependency of Auth.
Auth → Core → Auth → FrontendResponsibilities
Section titled “Responsibilities”Core:
- returns company entitlements
- returns enabled modules
- returns entitlement version
Auth:
- merges entitlements with user grants
- computes effective access
- returns final access model
Forbidden usage
Section titled “Forbidden usage”Core must NEVER:
- return user-level access
- validate permissions
- validate roles
- decide if a request is allowed
32. Effective access boundary
Section titled “32. Effective access boundary”Core MUST NOT implement:
effective_access = entitlements ∩ grantsThis logic belongs ONLY to Auth.
33. Delegation awareness (READ-ONLY CONTEXT)
Section titled “33. Delegation awareness (READ-ONLY CONTEXT)”Core is aware that delegation exists, but:
- does not enforce delegation
- does not validate delegation
- does not store delegation rules
Delegation is owned by Auth.
34. Frontend interaction rule
Section titled “34. Frontend interaction rule”Frontend must NOT call Platform Core directly for access.
Allowed frontend calls:
- Auth
- Base backend
- module backends
- admin backend
35. Backend interaction rule
Section titled “35. Backend interaction rule”Business backends must NOT:
- call Core for user access decisions
- use Core as authorization service
They must rely on:
Auth → /auth/me/access36. Final enforcement rule
Section titled “36. Final enforcement rule”Core = commercial truthAuth = access truthBackend = enforcementFrontend = UX only