Skip to main content

Module Development — Getting Started

A module is an independently deployable unit of business capability built on top of Platform-Kernel primitives. Once registered, a module appears in the UI Shell sidebar, runs in its own Module Federation 2.0 remote, and communicates with the kernel exclusively through the @platform/sdk-* packages.

This guide describes the full developer lifecycle from scaffold to production.


Prerequisites

ToolVersionNotes
Node.js≥ 24 LTSUse fnm or nvm for version management
pnpm≥ 10npm install -g pnpm
Docker + Compose≥ 26Required for the local sandbox
Go≥ 1.24Required only if you extend backend services

No Go installation is needed for pure frontend/SDK module development.


Step 1 — Start the Developer Sandbox

The sandbox is a full single-machine replica of the production kernel stack. It provides pre-seeded tenants, users, and roles so you can build without any manual infrastructure setup.

git clone https://github.com/septemcore/platform-kernel.git
cd platform-kernel/.dev/kernel

# Start the full stack: PostgreSQL, ClickHouse, Kafka, RabbitMQ,
# Valkey, Vault, and all 12 Go services
docker compose -f docker/docker-compose.yml up -d

# Check readiness
docker compose -f docker/docker-compose.yml ps

When all containers show healthy:

ServiceEndpointNotes
API Gatewayhttp://localhost:8080Single REST entry point
UI Shellhttp://localhost:3000Admin panel — demo tenant pre-seeded
UI Shell login[email protected] / Demo1234!Demo credentials

Step 2 — Scaffold Your Module

Use the official CLI to generate a fully configured module skeleton:

npx @platform/create-module@latest my-crm --description "A simple CRM module"
cd my-crm
pnpm install

The generator creates:

my-crm/
├── module.manifest.json ← Module contract (read by the Registry)
├── federation.config.js ← Module Federation 2.0 remote config
├── package.json
├── tsconfig.json
├── src/
│ └── pages/
│ └── index.tsx ← Main React 19 entry component
└── migrations/ ← SQL migrations (pressly/goose format)

Step 3 — Understand the Module Manifest

The module.manifest.json is the source of truth for the Module Registry. Every permission, event subscription, and data model your module uses must be declared here — undeclared access is rejected at the API Gateway level.

{
"name": "@demo/my-crm",
"version": "1.0.0",
"description": "A simple CRM module",
"kernelSdkVersion": "^1.0.0",
"entry": "",
"exposedComponent": "MyCrmModule",
"route": "/crm",
"icon": "Users",
"category": "crm",
"permissions": ["crm.contacts.read", "crm.contacts.write"],
"events": {
"publishes": ["crm.contact.created"],
"subscribes": ["auth.user.created"]
},
"dataApi": {
"models": ["contacts", "deals"]
},
"healthCheck": "/health",
"dependencies": {
"@platform/sdk-auth": "^1.0.0",
"@platform/sdk-data": "^1.0.0"
}
}

Required Manifest Fields

FieldTypeRule
namestringnpm-scope format: @scope/module-name
versionstringStrict Semver: X.Y.Z
descriptionstringNon-empty
entrystringSet automatically by the Registry on install
exposedComponentstringExported React component name
routestringURL path in the sidebar
iconstringIcon name from the SDK-UI icon catalog
permissionsstring[]Min 1 item; format: lowercase.dot.notation
dependenciesobjectMin @platform/sdk-core
healthCheckstringPath for the kernel health probe
kernelSdkVersionstringSemver range: "^1.0.0"

Maximum manifest size: 64 KB.

Optional Manifest Fields

FieldTypeDescription
events.publishesstring[]Kafka topics the module emits
events.subscribesstring[]Kafka topics the module consumes
dataApi.modelsstring[]Auto-generate REST CRUD endpoints
dataApi.hooksobjectLifecycle hooks: model.create.before
categorystringSidebar group: crm, analytics, finance, hr, marketing, custom
notificationChannelsarrayCustom notification channel adapters
authProvidersarrayCustom OAuth/wallet provider registration
embedsarrayEmbeddable widget definitions
dashboardWidgetobjectDashboard tile: { component, title, defaultSize }

Step 4 — Install SDK Packages

Install only the primitives your module needs:

# Core package (always required)
pnpm add @platform/sdk-core

# Add primitives as needed
pnpm add @platform/sdk-auth # Auth, RBAC, session management
pnpm add @platform/sdk-data # CRUD, realtime, analytics queries
pnpm add @platform/sdk-events # Kafka / browser event publish & subscribe
pnpm add @platform/sdk-notify # Notifications (WebSocket, email, SMS)
pnpm add @platform/sdk-files # File upload, image processing, thumbnails
pnpm add @platform/sdk-money # Wallets, credit/debit, holds, transfers
pnpm add @platform/sdk-audit # Audit trail recording

# UI Design System (optional)
pnpm add @platform/sdk-ui

# Development tools
pnpm add -D @platform/sdk-testing # Vitest mocks and fixtures

SDK to Primitive Mapping

SDK PackageBackend ServiceKey Exports
@platform/sdk-authIAMuseAuth(), useRBAC(), requireRole()
@platform/sdk-dataData LayeruseQuery(), useMutation(), useRealtime()
@platform/sdk-eventsEvent Buspublish(), subscribe(), onEvent()
@platform/sdk-notifyNotifysend(), sendBatch(), sendFromTemplate()
@platform/sdk-filesFilesupload(), download(), getThumbnail()
@platform/sdk-moneyMoneycredit(), debit(), transfer(), getBalance()
@platform/sdk-auditAuditrecord(), getHistory()

Step 5 — Write Your First Component

Open src/pages/index.tsx. The Data primitive auto-generates REST endpoints for every model declared in dataApi.models:

import { useAuth } from "@platform/sdk-auth";
import { useQuery } from "@platform/sdk-data";

interface Contact {
id: string;
name: string;
email: string;
created_at: string;
}

export default function MyCrmModule() {
const { user, tenant } = useAuth();

// Resolves to: GET /api/v1/data/my-crm/contacts?limit=20
// RLS ensures only current tenant's rows are returned.
const { data: contacts, isLoading } = useQuery<Contact>(
"my-crm",
"contacts",
{ limit: 20, order: "created_at.desc" }
);

if (isLoading) return <div>Loading...</div>;

return (
<div>
<h1>{tenant.name} — CRM</h1>
<p>Signed in as {user.email}</p>
<ul>
{contacts?.data.map((c) => (
<li key={c.id}>{c.name}{c.email}</li>
))}
</ul>
</div>
);
}

Publish an Event

When a contact is created, publish a domain event so other modules can react:

import { publish } from "@platform/sdk-events";

await publish({
topic: "platform.module.events",
type: "crm.contact.created",
entityId: contact.id,
tenantId,
payload: { contactId: contact.id, name: contact.name },
idempotencyKey: crypto.randomUUID(),
});

The entityId is used as the Kafka partition key. Declare "crm.contact.created" under events.publishes in your manifest before publishing.


Step 6 — Module Federation Configuration

Every module must have a federation.config.js that declares shared singleton dependencies. Duplicate copies of React or Zustand cause runtime context bugs:

import { defineConfig } from "@module-federation/vite";

export default defineConfig({
name: "crm",
exposes: {
"./App": "./src/App",
},
shared: {
react: { singleton: true, requiredVersion: "^19.0.0" },
"react-dom": { singleton: true, requiredVersion: "^19.0.0" },
"react-router-dom": { singleton: true, requiredVersion: "^7.0.0" },
zustand: { singleton: true, requiredVersion: "^5.0.0" },
"@platform/sdk-core": { singleton: true },
"@platform/sdk-auth": { singleton: true },
},
});

Step 7 — Register and Activate

With the sandbox running, start your dev server and register:

# Start the dev server (hot-reload, proxied to sandbox API)
pnpm dev
# → Remote available at http://localhost:5173/remoteEntry.js

# Register the module (reads module.manifest.json automatically)
npx @platform/create-module register \
--gateway http://localhost:8080 \
--token <admin_jwt_from_sandbox> \
--entry http://localhost:5173/remoteEntry.js

# Activate for the demo tenant
npx @platform/create-module activate \
--gateway http://localhost:8080 \
--token <admin_jwt_from_sandbox>

Open the UI Shell at http://localhost:3000 — your module appears in the sidebar. Changes in src/ hot-reload without restarting the shell.

Registration State Machine

POST /api/v1/modules/install
→ pending → installing → active / failed
StateMeaning
pendingModule queued for installation
installingRegistry downloading bundle, running SQL migrations
activeModule visible in UI Shell, all SDK calls authorized
failedInstallation failed; retry: POST /modules/:id/retry-install

Timeout: MODULE_INSTALL_TIMEOUT_SEC = 120 seconds.


Step 8 — Publish to Production

Build the production bundle and publish:

pnpm build
# Generates: dist/remoteEntry.js, dist/assets/, dist/module.manifest.json

npx @platform/create-module publish \
--gateway https://api.septemcore.com \
--token <platform_owner_jwt>

The Registry runs a bundle integrity check (SRI SHA-384) to prevent supply chain attacks. The CDN URL is generated automatically:

https://cdn.platform.io/modules/{tenantId}/{moduleId}/{version}/remoteEntry.js

Each version is an immutable deployment. Rollback is instant — the Registry switches the entry URL back to the previous version without republishing.


Development Workflow

CommandWhat it does
pnpm devLocal MF remote + API proxy to sandbox
pnpm buildProduction bundle (Vite 8)
pnpm testVitest unit tests
pnpm typecheckTypeScript strict check

SQL Migrations

Place versioned SQL files in migrations/:

migrations/
├── 001_create_contacts.sql
└── 002_add_contact_tags.sql

Migrations use pressly/goose format. They run automatically during installing state — the Module Registry runs goose up against the module's isolated schema. In emergencies:

npx @platform/cli module migration force-version \
--module my-crm --version 1

This command maps to kernel-cli migration force-version and clears the dirty flag without re-running the migration.


RBAC: Declaring Permissions

Permissions declared in manifest.permissions[] are automatically created in the IAM service at activation time. Use the dot-notation format <module>.<resource>.<action>:

"permissions": [
"crm.contacts.read",
"crm.contacts.write",
"crm.deals.read",
"crm.deals.write"
]

Check permissions in your component:

import { useRBAC } from "@platform/sdk-auth";

const { can } = useRBAC();
if (can("crm.contacts.write")) {
// Show edit button
}

Rollback

If a newly deployed version fails its health check within 60 seconds, the Module Registry executes an automatic rollback to the previous version. Manual rollback via the Admin UI:

  • Navigate to Module Registry → your module → Version History.
  • Select a previous version → Rollback.

The Registry switches the entry CDN URL instantly. Users online see a toast: "Module updated. Refresh to apply."


See Also