SeptemCore LogoSeptemCore
Modules

Module Development — Getting Started

Complete guide to building a Platform-Kernel module: scaffold with create-module, write manifest, use SDK primitives, register with the Module Registry, and deploy via immutable CDN versioning.

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.26.1Required 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?page_size=20
  // RLS ensures only current tenant's rows are returned.
  const { data: contacts, isLoading } = useQuery<Contact>(
    "my-crm",
    "contacts",
    { page_size: 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

Loading diagram...
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

On this page