Manifest and Routes
App manifest schema, route registration, and publish metadata.
Audience: Federated app developers and Shell host team
Stored in: Control Plane Postgres (apps.manifest_json)
Validated by: app/domain/entities/shell_manifest.py when app_kind = shell_federated
The manifest is the contract between Portal, Control Plane bootstrap API, and the Shell host. Extra keys are allowed until the joint full schema is locked.
Identity: slug vs appId
| Field | Where | Purpose |
|---|---|---|
slug | App row (apps.slug) | Portal URLs, bootstrap ?slug=, Shell path /apps/{slug}/… |
manifest.appId | Manifest JSON | Module Federation scope name |
app.id | UUID | Authenticated APIs, release storage keys |
Convention: keep slug and appId identical unless you have a deliberate reason not to. Bootstrap uses slug; webpack remotes use appId.
appId pattern: ^[a-zA-Z][a-zA-Z0-9._-]{0,62}$
Required fields (Phase 1)
| Field | Type | Description |
|---|---|---|
version | string | Schema version, e.g. "1.0" |
appId | string | Federation scope (see pattern above) |
name | string | Display name (max 120 chars) |
routes | array | Navigation entries (see below) |
theme | object | Shell chrome colors |
Route object
| Field | Required | Description |
|---|---|---|
path | Yes | Route segment under the app (e.g. home, settings) |
title | Yes | Sidebar / nav label |
icon | No | Icon name (Shell maps to lucide or design system) |
layout | No | Future layout blocks; default [] |
Shell URL convention:
/apps/{slug}/{route.path}Example: slug my-app, route path page-1 → /apps/my-app/page-1
The federated remote must export a module matching the route contract your Shell host expects (see Shell starter-kit CONTEXT.md).
Theme object
| Field | Required | Description |
|---|---|---|
primaryColor | Yes | CSS hex, e.g. #2563EB |
accentColor | Yes | CSS hex, e.g. #7C3AED |
mode | No | "light" or "dark" (default "light") |
Shell applies these to CSS variables / design tokens for app chrome around the federated remote.
Set on publish
| Field | Type | Description |
|---|---|---|
remoteUrl | string (URL) | Full URL to remoteEntry.js after Portal publish |
Empty before first publish. Shell uses SHELL_REMOTE_DEV_URL in dev when empty.
Optional fields (pass-through)
| Field | Purpose |
|---|---|
displayName | Alternate display string |
description | App blurb |
icon | App-level icon |
collections | Hints for data collections the app uses (nav / docs) |
Draft fields (joint schema session — not validated yet)
These may appear in manifests today but are not enforced by Control Plane validation. Documented for alignment with future Coderunner/Shell unified schema:
| Field | Purpose |
|---|---|
runtime | python | nodejs | dotnet | nextjs |
build.install / build.start | Coderunner build commands |
env_required | Secret keys the app expects |
resources | CPU/memory/replica hints |
See docs/HANDOVER-AMIN.md Ask 5 for the proposed merged shape.
Sample manifest
From Portal apps/customer/src/lib/apps/manifest.ts:
{
"version": "1.0",
"appId": "my-app-2",
"name": "My App 2",
"displayName": "My App 2",
"description": "Starter remote; Shell federated app.",
"icon": "Sparkles",
"theme": {
"primaryColor": "#2563EB",
"accentColor": "#7C3AED",
"mode": "light"
},
"collections": [],
"routes": [
{
"path": "page-1",
"title": "Page 1",
"icon": "FileText",
"layout": []
}
],
"remoteUrl": ""
}Register and update (Portal / API)
Register:
POST /v1/workspaces/{workspace_id}/apps
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "My App",
"slug": "my-app",
"app_kind": "shell_federated",
"manifest": { … }
}Patch manifest:
PATCH /v1/workspaces/{workspace_id}/apps/{app_id}
Authorization: Bearer {token}
Content-Type: application/json
{ "manifest": { … } }Validation runs on register/patch for shell_federated apps. Invalid appId or missing required fields return 422.
Publish flow (updates remoteUrl)
Publishing does not replace the whole manifest — it adds/updates remoteUrl and creates an app_releases row.
POST …/apps/{app_id}/releases/upload-url— presigned PUT for bundle- Client PUTs
remoteEntry.jsor.zip POST …/apps/{app_id}/releases/{release_id}/finalize— extracts zip if needed, writesremoteUrlinto manifest
Bootstrap returns the latest published release plus current manifest.
Module Federation mapping
const scope = config.manifest.appId;
const entry = config.manifest.remoteUrl; // full URL to remoteEntry.js
// remotes[scope] = `${base}@${entry}` — see 02-shell-bootstrap.mdRemote package name / exposed modules must match what Shell imports (starter-kit documents the exact export names).
Validation errors (common)
| Error | Fix |
|---|---|
appId pattern mismatch | Start with letter; use [a-zA-Z0-9._-] only |
Missing routes or empty route path/title | Add at least one route for Shell nav |
Missing theme.primaryColor | Include full theme object |
| Bootstrap 400 not shell_federated | Re-register with correct app_kind |
Related
- 01-getting-started.md — end-to-end walkthrough
- 02-shell-bootstrap.md — consume manifest at runtime
- 12-troubleshooting.md — publish and bootstrap issues