Skip to main content

Project Structure

Every Platform-Kernel module is a self-contained JavaScript/TypeScript package that is deployed as a Module Federation 2.0 remote. The scaffold generated by npx @platform/create-module produces the canonical structure. Deviation from this structure is permitted, but the files listed as required must exist exactly as specified.


Full Directory Tree

my-module/
├── module.manifest.json ← [REQUIRED] Module contract — read by the kernel
├── federation.config.js ← [REQUIRED] Module Federation 2.0 remote config
├── package.json ← [REQUIRED] npm package descriptor
├── tsconfig.json ← TypeScript configuration

├── src/
│ ├── pages/
│ │ └── index.tsx ← [REQUIRED] Main React entry component (exposed via MF)
│ ├── engines/ ← Business logic (pure TS, no React)
│ │ └── contacts.ts
│ ├── api/ ← Custom API client helpers (thin wrappers over SDK)
│ │ └── contacts.api.ts
│ ├── events/ ← Event handlers (subscribes declared in manifest)
│ │ └── onUserCreated.ts
│ └── components/ ← Module-private React components
│ └── ContactCard.tsx

├── migrations/ ← [REQUIRED if dataApi used] SQL goose migrations
│ └── 001_create_contacts.sql

└── tests/
├── unit/ ← Vitest unit tests (sdk-testing mocks)
│ └── contacts.engine.test.ts
└── integration/ ← Optional: Testcontainers-based integration tests
└── contacts.integration.test.ts

Root-Level Files

module.manifest.json — The Module Contract

The manifest is the single most important file in your module. The kernel reads it at registration time to configure routing, permissions, event subscriptions, data models, and billing limits. Nothing your module does at runtime is permitted unless it is declared here.

See Module Manifest for the complete field reference. Key fields at a glance:

{
"name": "@scope/my-module",
"version": "1.0.0",
"description": "Human-readable description for the Module Registry",
"kernelSdkVersion": "^1.0.0",
"entry": "",
"exposedComponent": "MyModuleComponent",
"route": "/my-module",
"icon": "LayoutDashboard",
"permissions": ["my-module.resource.read", "my-module.resource.write"],
"events": {
"publishes": ["my-module.item.created"],
"subscribes": ["auth.user.created"]
},
"healthCheck": "/health",
"dependencies": {
"@platform/sdk-core": "^1.0.0"
}
}
Naming convention for name

The name field must follow npm-scope format: @scope/module-name (lowercase, hyphen-separated). Regex: ^@[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$. Registration fails with 400 Bad Request if the format is invalid.

Naming convention for permissions

Each permission string must follow lowercase.dot.notation format: {module-slug}.{resource}.{action}. Example: crm.contacts.read. The kernel auto-registers these in IAM when the module activates — you do not create them manually.


federation.config.js — Module Federation Remote

Configures your module as a Module Federation 2.0 remote. The UI Shell (host) discovers and loads this entry point dynamically from the Module Registry.

import { EnterpriseSingletons } from '@kernel/build-config';

export default {
name: 'my_module', // must be a valid JS identifier (underscores)
exposes: {
'./Module': './src/pages/index.tsx', // the component the shell mounts
},
// Shared singleton dependencies (React, Zustand, etc.) are managed centrally
// in @kernel/build-config. Do NOT hardcode requiredVersion here.
shared: EnterpriseSingletons,
};
Do not hardcode requiredVersion in shared

Singleton dependency versions are managed in @kernel/build-config/src/mf-singletons.ts — the single source of truth across all modules. Hardcoding versions here risks runtime conflicts that cause the shell to raise an Error Boundary instead of loading your module.


package.json — Package Descriptor

{
"name": "@platform/module-my-module",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "platform-dev",
"build": "tsc && vite build",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"zustand": "5.0.3",
"@platform/sdk-core": "workspace:*",
"@platform/sdk-auth": "workspace:*",
"@platform/sdk-data": "workspace:*"
},
"devDependencies": {
"@platform/dev-server": "workspace:*",
"@platform/tsconfig": "workspace:*",
"@platform/sdk-testing": "workspace:*",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"typescript": "5.8.2",
"vite": "6.2.0"
}
}

The platform-dev command (from @platform/dev-server) starts a Vite dev server pre-configured with:

  • Module Federation remote mode
  • API proxy pointing to http://localhost:8080 (sandbox Gateway)
  • HMR and TypeScript incremental compilation

Source Directory (src/)

src/pages/index.tsx — Module Entry Point

This is the only component exposed via Module Federation. The UI Shell mounts this component when the user navigates to your module's route. It must be a valid React default export.

// The entry component receives no props from the shell.
// All contextual data (user, tenant, permissions) is accessed via SDK hooks.
export default function MyModuleComponent() {
return <div>My Module</div>;
}

The shell wraps every mounted component in:

  1. A per-module Error Boundary — a crash in your component shows an error card instead of breaking the shell
  2. A Suspense boundary — the shell shows a skeleton loader while your bundle downloads

src/engines/ — Business Logic

Pure TypeScript functions with no React dependencies. This layer contains your domain logic: validation rules, data transformations, computation. Keeping business logic here (rather than inside components) makes it independently testable with Vitest.

// src/engines/contacts.ts
export function validateContactEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export function normalisePhoneNumber(phone: string): string {
return phone.replace(/\D/g, '');
}

src/api/ — SDK Wrappers

Thin API client functions that wrap SDK calls with module-specific defaults. Centralise error handling, default query parameters, and response mapping here — keep components clean.

// src/api/contacts.api.ts
import { useQuery, useMutation } from '@platform/sdk-data';

// Module slug is 'my-module', model name is 'contacts'.
// Resolves to: GET https://api.septemcore.com/api/v1/data/my-module/contacts
export const useContacts = () =>
useQuery('my-module', 'contacts', { limit: 50, order: 'created_at.desc' });

export const useCreateContact = () =>
useMutation('my-module', 'contacts', 'create');

src/events/ — Event Handlers

Handlers for Kafka/RabbitMQ events your module subscribes to. Each file should handle one event type declared in manifest.events.subscribes.

// src/events/onUserCreated.ts
import { subscribe } from '@platform/sdk-events';
import type { AuthUserCreatedEvent } from '@platform/sdk-events/types';

// This handler is called when auth.user.created fires in Kafka.
// Only fires for events scoped to your tenant (SDK enforces tenant isolation).
subscribe('auth.user.created', async (event: AuthUserCreatedEvent) => {
// e.g., create a default contact record for every new user
console.log('New user registered:', event.data.userId);
});
Subscription RBAC

Your module can only subscribe to events declared in manifest.events.subscribes. Attempting to subscribe to an undeclared event at runtime results in a 403 Forbidden from the Event Bus SDK. This is enforced server-side, not just client-side.


src/components/ — Module-Private Components

React components used internally by your module. These are not shared via Module Federation — only the entry component (src/pages/index.tsx) is exposed. Component-to-component sharing across modules must go through the Event Bus or Data primitives, never through MF shared state.


Migrations Directory (migrations/)

Required if your module uses the Data primitive (dataApi field in manifest). Each migration is a plain SQL file following pressly/goose numbering format.

-- migrations/001_create_contacts.sql
-- +goose Up
CREATE TABLE module_my_module.contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL, -- always required; RLS is added automatically by kernel
name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ -- soft delete support
);

-- RLS policy is added automatically by the kernel — DO NOT add it manually

-- +goose Down
DROP TABLE IF EXISTS module_my_module.contacts;
Table namespace

All module tables live in their own PostgreSQL schema: module_{moduleSlug}. The kernel creates this schema automatically. Two modules can both have a contacts table — they are module_crm.contacts and module_sales.contacts, fully independent.

Migrations are forward-only

Automatic rollback of data migrations is prohibited — it risks destroying tenant data. Write your migrations defensively: new columns should have DEFAULT NULL, and your Go/TS code must handle both old and new schema shapes simultaneously. See Architecture → Data Flow for the migration failure handling policy.


Tests Directory (tests/)

Unit Tests (tests/unit/)

Use Vitest with @platform/sdk-testing mocks. All SDK primitives have pre-built mock factories:

// tests/unit/contacts.engine.test.ts
import { describe, it, expect } from 'vitest';
import { validateContactEmail } from '../../src/engines/contacts';

describe('validateContactEmail', () => {
it('accepts a valid email', () => {
expect(validateContactEmail('[email protected]')).toBe(true);
});

it('rejects a missing domain', () => {
expect(validateContactEmail('notanemail')).toBe(false);
});
});

To mock SDK calls in component tests:

import { mockKernel } from '@platform/sdk-testing';

// In your test setup
mockKernel({
auth: { user: { id: 'user-123', email: '[email protected]' } },
data: { 'my-module/contacts': [] },
});

File Naming and Conventions

RuleCorrectIncorrect
React componentsPascalCase.tsxcontactCard.tsx
Non-component TS filescamelCase.tsContactCard.ts
Event handlerson{EventName}.tshandle_user.ts
Migration filesNNN_description.sqlmigration.sql
Manifestmodule.manifest.jsonmanifest.json

Module Lifecycle

When you register and activate your module, the kernel executes this sequence:


Next Steps

DocumentWhat you will learn
Module ManifestEvery field in module.manifest.json with types and constraints
Dev EnvironmentFull Docker Compose stack and environment variables
Concepts → Module FederationHow the UI Shell discovers and loads your module
Primitives → DataData Layer CRUD, query language, and analytics