The Seven Primitives
The philosophy underpinning the SeptemCore Platform-Kernel can be stated in one sentence:
"Everything is a primitive." Any feature a module needs — authenticating a user, storing a record, sending a notification — is solved by composing exactly one or more of seven orthogonal kernel primitives. The kernel contains no business logic; modules contain no infrastructure code.
This separation is load-bearing. It means the kernel can be extended to thousands of industry-specific modules without a single line of kernel code changing.
Primitive Design Rules
Before diving into each primitive, it helps to understand the rules every primitive follows:
- SDK first — every primitive ships a TypeScript SDK package
(
@platform/sdk-{primitive}). Modules call primitives exclusively through the SDK; direct HTTP or gRPC calls are prohibited. - One backend service — each primitive maps to a dedicated Go microservice (or the Gateway, for cross-cutting concerns). No shared databases between primitives.
- Tenant isolation by default — every API call is automatically scoped
to the caller's
tenant_idextracted from the JWT. Modules cannot access another tenant's data. - OpenAPI 3.1.0 + gRPC dual contract — REST for external callers, gRPC for internal Gateway-to-service communication.
- Event Bus integration — every primitive publishes domain events to Kafka so other services can react without coupling.
The Seven Primitives at a Glance
| Primitive | SDK (Service) | Primary Storage | Kafka Topic |
|---|---|---|---|
| 1. Auth | @platform/sdk-auth (iam) | PostgreSQL + Valkey | platform.auth.events |
| 2. Data | @platform/sdk-data (data-layer) | PostgreSQL + ClickHouse + Valkey | platform.data.events |
| 3. Events | @platform/sdk-events (event-bus) | Kafka + RabbitMQ | (is the bus) |
| 4. Notify | @platform/sdk-notify (notify) | PostgreSQL + Valkey + RabbitMQ | platform.notify.events |
| 5. Files | @platform/sdk-files (files) | PostgreSQL + S3 | platform.files.events |
| 6. Money | @platform/sdk-money (money) | PostgreSQL (ACID) | platform.money.events |
| 7. Audit | @platform/sdk-audit (audit) | ClickHouse + PostgreSQL WAL | platform.audit.events |
Primitive 1 — Auth
Responsibility: everything related to identity, access, and trust.
| Capability | Detail |
|---|---|
| Authentication | Email + password (built-in), pluggable AuthProvider adapters (OAuth, wallet, social, banking, enterprise, custom) |
| Tokens | JWT ES256 (ECDSA P-256) — access 15 min, refresh 7 days with rotation |
| Password security | Argon2id (NIST 800-63B), minimum 12 characters |
| MFA | TOTP (RFC 6238), 10 argon2id-hashed recovery codes |
| RBAC | Granular, permission-based. Roles are user-defined; the only fixed role is Platform Owner (*) |
| OIDC Provider | Full OAuth 2.1 + OpenID Connect IdP — modules and mobile apps can use Sign in with SeptemCore |
| Multi-tenancy | TenantHierarchyService gRPC service (IsDescendant, GetTenantStatus) used by Gateway delegation middleware |
Key REST endpoints (all via https://api.septemcore.com/v1/):
POST /auth/register — create user + tenant (atomic transaction)
POST /auth/login — returns { access_token, refresh_token }
POST /auth/refresh — rotate token pair (10 s grace window for multi-tab safety)
GET /auth/me — current user + resolved permissions
POST /auth/mfa/enable — generate TOTP secret + QR code
POST /roles — create custom role with arbitrary permissions
POST /users/:id/roles — assign role to user
GET /permissions — list all registered permissions (kernel + modules)
SDK usage:
import { useAuth, useRBAC } from '@platform/sdk-auth';
const { user, logout } = useAuth();
const { hasPermission } = useRBAC();
if (hasPermission('crm.contacts.read')) {
// render the contacts table
}
Primitive 2 — Data
Responsibility: automatic CRUD + analytics API for module-defined SQL models.
A module declares its data models in module.manifest.json under the
dataApi field. The kernel generates five REST endpoints per model — no
additional code required.
| Auto-generated endpoint | Description |
|---|---|
POST /api/v1/data/{module}/{model} | Create record |
GET /api/v1/data/{module}/{model} | List with filters, sorting, cursor pagination |
GET /api/v1/data/{module}/{model}/:id | Retrieve by ID (supports ?select= for relations) |
PATCH /api/v1/data/{module}/{model}/:id | Partial update |
DELETE /api/v1/data/{module}/{model}/:id | Soft delete |
Query language (PostgREST-inspired):
GET https://api.septemcore.com/v1/data/crm/contacts
?status=eq.active
&price=gt.100
&order=created_at.desc
&cursor=eyJpZCI6IjAxOT...
&limit=20
&select=name,email,company(name)
Operational limits:
| Limit | Value | Env override |
|---|---|---|
| Max record size | 1 MB | DATA_MAX_RECORD_SIZE_BYTES |
| Max JSONB field | 256 KB | DATA_MAX_JSONB_SIZE_BYTES |
| Max tables per module | 50 | DATA_MAX_TABLES_PER_MODULE |
| Max relation depth | 2 levels | — |
Max limit per request | 100 | — |
CDC pipeline: PostgreSQL → Debezium (WAL) → Kafka → ClickHouse —
analytics lag < 5 s.
Permission reconciliation runs on every migration: new tables register
{module}.{model}.read/write/delete permissions automatically; removed
tables archive those permissions.
Controlled by DATA_PERMISSION_RECONCILIATION_ON_MIGRATE=true.
Primitive 3 — Events
Responsibility: asynchronous domain-event infrastructure. Kafka for ordered domain events, RabbitMQ for transactional task queues.
Modules declare which events they publish and subscribe to inside
module.manifest.json. The kernel enforces this contract at runtime:
publishing an undeclared event or subscribing to an undeclared event returns
403 Forbidden.
Kernel-owned topic namespaces (auth.*, money.*, billing.*, audit.*)
are hardcoded as write-protected — only kernel services can publish to them.
A module attempting to publish returns 403 Forbidden.
Kafka topics:
| Topic | Domain | Sample events |
|---|---|---|
platform.auth.events | IAM | auth.user.created, auth.role.changed |
platform.data.events | Data Layer | CDC events (Debezium) |
platform.money.events | Money | money.wallet.credited, money.wallet.debited |
platform.files.events | Files | files.file.uploaded |
platform.notify.events | Notify | notify.notification.sent |
platform.audit.events | Audit | audit.record.created |
platform.billing.events | Billing | billing.plan.changed |
platform.module.events | Module Registry | module.registry.registered, module.registry.activated |
Conventions: Topic: platform.{domain}.events. Partition key:
entityId. Consumer group: {consuming-service}.{handler-name}.
SDK usage:
import { publish, subscribe } from '@platform/sdk-events';
// Publish (must be declared in manifest.events.publishes)
await publish('crm.contact.created', { contactId: '01963...' });
// Subscribe (must be declared in manifest.events.subscribes)
subscribe('auth.user.created', async (event) => {
await createWelcomeRecord(event.payload.userId);
});
Frontend MFE-to-MFE communication uses Browser Custom Events for
fire-and-forget UI updates. When delivery order matters, use the Event Bus
backend path — Kafka guarantees ordering by partition key (entityId).
Primitive 4 — Notify
Responsibility: multi-channel notifications with a pluggable channel adapter system.
| Capability | Detail |
|---|---|
| Real-time | WebSocket (wss://api.septemcore.com/ws) with auth handshake, 30 s heartbeat, replay buffer (100 msgs, 1 h TTL backed by Valkey) |
| Channels | Built-in: WebSocket, Browser Push. Pluggable adapters: Email, SMS, Telegram, Discord, Slack, … |
| Templates | Template engine with per-tenant variables |
| Rate limits | 100 notifications/min per tenant, 50/min per module, batch max 500 |
| Retry | 5 retries, exponential backoff 30 s → 480 s, ±10 % jitter |
Key endpoints (via https://api.septemcore.com/v1/):
POST /notifications — send single notification
POST /notifications/batch — send up to 500 in one call
GET /notifications/:id — delivery status
POST /notifications/:id/retry — retry a failed notification
POST /notifications/templates — create reusable template
POST /notifications/channels — register a channel adapter
Primitive 5 — Files
Responsibility: secure binary file management with built-in antivirus scanning and image processing.
Every uploaded file passes through a staging bucket before promotion to the main bucket. The antivirus scanner runs in the staging bucket; infected files are rejected and never reach storage.
| Capability | Detail |
|---|---|
| Upload | Multipart (POST /files/upload) or presigned URL flow |
| Image processing | bimg / libvips — thumbnails at five presets: icon_32, avatar_64, card_300, preview_600, full_1200 |
| Soft delete | 30-day recycle bin (FILES_SOFT_DELETE_RETENTION_DAYS) |
| Staging TTL | 24 h (FILES_STAGING_TTL_HOURS) |
| S3 path | {tenantId}/{bucket}/ — per-tenant path prefix enforces isolation |
Primitive 6 — Money
Responsibility: ACID wallet ledger with hold/confirm/cancel, reversal, and double-entry bookkeeping.
All monetary values are stored as integers (cents / smallest currency unit)
in BIGINT columns. Floating-point arithmetic is never used.
Transaction guarantees:
| Guarantee | Implementation |
|---|---|
| Atomicity | PostgreSQL transactions, SELECT FOR UPDATE (pessimistic locking) |
| Deadlock prevention | Wallets locked in ascending UUID order |
| Idempotency | Idempotency-Key header required on all mutating calls |
| Double-entry | Every credit creates a corresponding debit entry |
| Hold lifecycle | Hold → confirm (debit) or cancel (release). Default hold TTL: 72 h |
| Reversal window | Up to 365 days (MONEY_REVERSAL_MAX_AGE_DAYS) |
Operational limits: max transaction $100 K, max balance $1 M (overridden per Billing plan), min 1 cent.
Primitive 7 — Audit
Responsibility: immutable, tamper-evident event log for compliance, GDPR, and forensic investigation.
Audit writes use a dual-write pipeline for zero data loss:
Primary: Service → Kafka (platform.audit.events) → ClickHouse consumer → ClickHouse
Fallback: Kafka down → PostgreSQL audit_wal table → Background replay → Kafka
| Tier | Storage | Retention |
|---|---|---|
| Hot | ClickHouse | 90 days (AUDIT_HOT_DAYS) |
| Cold | S3 Glacier | Up to 7 years (AUDIT_COLD_YEARS) |
| GDPR | ReplacingMergeTree FINAL anonymisation | On-demand via POST /audit/anonymize |
Combining Primitives: A Real-World Example
The following shows how a hypothetical CRM module uses all seven primitives to implement a single "Create Contact" user action:
// 1. AUTH — verify the caller has permission
const { hasPermission } = useRBAC();
if (!hasPermission('crm.contacts.create')) throw new ForbiddenError();
// 2. DATA — persist the contact (auto-CRUD via dataApi manifest field)
// POST https://api.septemcore.com/v1/data/crm/contacts
const contact = await kernel.data('crm').create('contacts', payload);
// 3. FILES — attach the contact's avatar
const avatar = await kernel.files().upload(avatarFile, { bucket: 'avatars' });
await kernel.data('crm').update('contacts', contact.id, { avatarId: avatar.id });
// 4. EVENTS — broadcast so other modules can react
await kernel.events().publish('crm.contact.created', { contactId: contact.id });
// 5. NOTIFY — send a welcome email via the Notify primitive
await kernel.notify().send({
channel: 'email',
to: contact.email,
templateId: 'crm.welcome',
variables: { name: contact.name },
});
// 6. MONEY — initialise a credit account for the contact (optional)
await kernel.money().createWallet({ ownerId: contact.id, currency: 'USD' });
// 7. AUDIT — immutably record the action
await kernel.audit().record({
action: 'crm.contact.created',
entityId: contact.id,
actorId: user.id,
});
Every step calls a kernel primitive through the SDK. The CRM module contains no database connection strings, no S3 credentials, no SMTP configuration — the kernel provides all of this transparently, with multi-tenancy and security baked in.