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
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 24 LTS | Use fnm or nvm for version management |
| pnpm | ≥ 10 | npm install -g pnpm |
| Docker + Compose | ≥ 26 | Required for the local sandbox |
| Go | ≥ 1.24 | Required 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:
| Service | Endpoint | Notes |
|---|---|---|
| API Gateway | http://localhost:8080 | Single REST entry point |
| UI Shell | http://localhost:3000 | Admin 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
| Field | Type | Rule |
|---|---|---|
name | string | npm-scope format: @scope/module-name |
version | string | Strict Semver: X.Y.Z |
description | string | Non-empty |
entry | string | Set automatically by the Registry on install |
exposedComponent | string | Exported React component name |
route | string | URL path in the sidebar |
icon | string | Icon name from the SDK-UI icon catalog |
permissions | string[] | Min 1 item; format: lowercase.dot.notation |
dependencies | object | Min @platform/sdk-core |
healthCheck | string | Path for the kernel health probe |
kernelSdkVersion | string | Semver range: "^1.0.0" |
Maximum manifest size: 64 KB.
Optional Manifest Fields
| Field | Type | Description |
|---|---|---|
events.publishes | string[] | Kafka topics the module emits |
events.subscribes | string[] | Kafka topics the module consumes |
dataApi.models | string[] | Auto-generate REST CRUD endpoints |
dataApi.hooks | object | Lifecycle hooks: model.create.before |
category | string | Sidebar group: crm, analytics, finance, hr, marketing, custom |
notificationChannels | array | Custom notification channel adapters |
authProviders | array | Custom OAuth/wallet provider registration |
embeds | array | Embeddable widget definitions |
dashboardWidget | object | Dashboard 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 Package | Backend Service | Key Exports |
|---|---|---|
@platform/sdk-auth | IAM | useAuth(), useRBAC(), requireRole() |
@platform/sdk-data | Data Layer | useQuery(), useMutation(), useRealtime() |
@platform/sdk-events | Event Bus | publish(), subscribe(), onEvent() |
@platform/sdk-notify | Notify | send(), sendBatch(), sendFromTemplate() |
@platform/sdk-files | Files | upload(), download(), getThumbnail() |
@platform/sdk-money | Money | credit(), debit(), transfer(), getBalance() |
@platform/sdk-audit | Audit | record(), 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
| State | Meaning |
|---|---|
pending | Module queued for installation |
installing | Registry downloading bundle, running SQL migrations |
active | Module visible in UI Shell, all SDK calls authorized |
failed | Installation 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
| Command | What it does |
|---|---|
pnpm dev | Local MF remote + API proxy to sandbox |
pnpm build | Production bundle (Vite 8) |
pnpm test | Vitest unit tests |
pnpm typecheck | TypeScript 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
- Quickstart — five-minute hello-world walkthrough
- First Deployment — full canary release and rollback walkthrough
- Module Federation 2.0 — host and remote architecture
- Module Registry — Overview — lifecycle state machine and gRPC API
- Concepts — Primitives — seven building blocks explained
- Versioning and Deprecation — semver policy and sunset rules