Shell Bootstrap
Environment variables, bootstrap URL, and Module Federation wiring for the Shell host.
Audience: Shell host (Next.js + Module Federation)
Full guide: docs/shell-controlplane-integration.md (same content, expanded)
The Shell host loads federated app metadata from Control Plane at runtime. It must not read Control Plane Postgres or object-store credentials directly.
Resolution flow
Browser → Shell host
1. Resolve workspace_id (env or hostname map)
2. Resolve app_slug from URL path (e.g. /apps/my-app/page-1)
3. GET /v1/public/workspaces/{workspace_id}/apps/by-slug?slug={app_slug}
4. Build Module Federation remotes from manifest.remoteUrl
5. Browser loads remoteEntry.js from object storePublic bootstrap API
GET {CONTROLPLANE_URL}/v1/public/workspaces/{workspace_id}/apps/by-slug?slug={app_slug}No authentication. Rate limit: RATE_LIMIT_PUBLIC_SHELL (default 120/minute per client IP).
Example:
curl -s "http://localhost:8088/v1/public/workspaces/${WORKSPACE_ID}/apps/by-slug?slug=my-app" | jq .Response shape (ShellConfigResponse):
| Field | Purpose |
|---|---|
app_id | UUID — storage keys, authenticated shell-config path |
slug | URL segment and ?slug= parameter |
app_kind | Must be shell_federated for Shell Phase 1 |
manifest.appId | Module Federation scope name |
manifest.routes | Sidebar / navigation |
manifest.theme | Colors and light/dark mode |
manifest.remoteUrl | Full URL to remoteEntry.js (after publish) |
release | Latest publish record, or null if never published |
| HTTP | Meaning |
|---|---|
200 | App found |
404 | Unknown slug or archived app |
400 | App exists but app_kind is not shell_federated |
Before first publish: release is null, manifest.remoteUrl is often empty — use SHELL_REMOTE_DEV_URL in dev.
Environment variables (Shell host)
| Variable | Required | Example | Purpose |
|---|---|---|---|
CONTROLPLANE_URL | Yes | http://localhost:8088 | API base (no trailing slash) |
SHELL_WORKSPACE_ID | Dev yes | UUID from Portal workspace URL | Workspace that owns the app |
SHELL_APP_SLUG | Optional | my-app | Default app when path omits slug |
SHELL_REMOTE_DEV_URL | Dev recommended | http://localhost:3002/remoteEntry.js | Fallback when not published |
Production: resolve SHELL_WORKSPACE_ID from hostname at deploy time, not from client bundles.
Module Federation
const remoteUrl =
config.manifest.remoteUrl?.trim() ||
process.env.SHELL_REMOTE_DEV_URL;
if (!remoteUrl) {
throw new Error("App is not published and SHELL_REMOTE_DEV_URL is unset");
}
const moduleName = config.manifest.appId;
// webpack / NextFederationPlugin — exact API depends on your bundler
remotes: {
[moduleName]: `${remoteUrl.replace(/\/remoteEntry\.js$/, "")}@${remoteUrl}`,
}remoteUrl is the full URL to remoteEntry.js — use as-is.
TypeScript helper
export interface ShellConfigResponse {
app_id: string;
slug: string;
app_kind: string;
manifest: {
version: string;
appId: string;
name: string;
remoteUrl?: string;
routes: Array<{ path: string; title: string; icon?: string }>;
theme: { primaryColor: string; accentColor: string; mode?: "light" | "dark" };
[key: string]: unknown;
};
release: { build_number: number; remote_url: string; status: string; created_at: string } | null;
}
export async function fetchPublicShellConfig(
workspaceId: string,
appSlug: string,
): Promise<ShellConfigResponse> {
const base = (process.env.CONTROLPLANE_URL ?? "http://localhost:8088").replace(/\/$/, "");
const url = `${base}/v1/public/workspaces/${workspaceId}/apps/by-slug?${new URLSearchParams({ slug: appSlug })}`;
const res = await fetch(url, { next: { revalidate: 60 } });
if (!res.ok) {
throw new Error(`Shell bootstrap failed: ${res.status} ${await res.text()}`);
}
return res.json();
}Or import types from @groundfloor/api-client (ShellConfigResponse).
Authenticated bootstrap (optional)
When Shell runs behind the same Keycloak session as the Portal (e.g. admin preview):
GET /v1/workspaces/{workspace_id}/apps/{app_id}/shell-config
Authorization: Bearer {keycloak_access_token}Same response shape as public bootstrap. Requires read on the workspace.
Checklist
Control Plane / Portal
- App registered with
app_kindshell_federated - Manifest includes
routes+theme - At least one successful Publish (
remoteEntry.jsor zip withremoteEntry.jsat root) - App status active;
manifest.remoteUrlnon-empty
Shell host
-
CONTROLPLANE_URLconfigured - Resolve
workspace_idandapp_slug - Fetch bootstrap on layout load (cache appropriately)
- Navigation from
manifest.routes; theme frommanifest.theme - Module Federation with
manifest.appId+manifest.remoteUrl - Dev fallback to
SHELL_REMOTE_DEV_URL - Handle 404 and unpublished state
Verify
curl -s "${CONTROLPLANE_URL}/v1/public/workspaces/${SHELL_WORKSPACE_ID}/apps/by-slug?slug=${SHELL_APP_SLUG}" | jq .
curl -sI "$(jq -r .manifest.remoteUrl <<<"$json")" | head -1 # expect HTTP 200Portal app detail → Shell bootstrap card shows the same URLs.
Publishing (from Portal, not Shell)
- Build federated remote →
remoteEntry.js - Portal → workspace → app → Publish build
- Re-fetch bootstrap —
manifest.remoteUrlandreleaseshould update
Roadmap (not in Phase 1)
| Item | Status |
|---|---|
GET /v1/public/workspaces/by-host?host= | Planned — use hostname → UUID map |
Coderunner app_kind | Shell does not load these apps |
| Workspace site end-user auth | Separate from bootstrap — see 03-authentication.md |