Groundfloor Docs

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 store

Public 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):

FieldPurpose
app_idUUID — storage keys, authenticated shell-config path
slugURL segment and ?slug= parameter
app_kindMust be shell_federated for Shell Phase 1
manifest.appIdModule Federation scope name
manifest.routesSidebar / navigation
manifest.themeColors and light/dark mode
manifest.remoteUrlFull URL to remoteEntry.js (after publish)
releaseLatest publish record, or null if never published
HTTPMeaning
200App found
404Unknown slug or archived app
400App 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)

VariableRequiredExamplePurpose
CONTROLPLANE_URLYeshttp://localhost:8088API base (no trailing slash)
SHELL_WORKSPACE_IDDev yesUUID from Portal workspace URLWorkspace that owns the app
SHELL_APP_SLUGOptionalmy-appDefault app when path omits slug
SHELL_REMOTE_DEV_URLDev recommendedhttp://localhost:3002/remoteEntry.jsFallback 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_kind shell_federated
  • Manifest includes routes + theme
  • At least one successful Publish (remoteEntry.js or zip with remoteEntry.js at root)
  • App status active; manifest.remoteUrl non-empty

Shell host

  • CONTROLPLANE_URL configured
  • Resolve workspace_id and app_slug
  • Fetch bootstrap on layout load (cache appropriately)
  • Navigation from manifest.routes; theme from manifest.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 200

Portal app detail → Shell bootstrap card shows the same URLs.


Publishing (from Portal, not Shell)

  1. Build federated remote → remoteEntry.js
  2. Portal → workspace → app → Publish build
  3. Re-fetch bootstrap — manifest.remoteUrl and release should update

Roadmap (not in Phase 1)

ItemStatus
GET /v1/public/workspaces/by-host?host=Planned — use hostname → UUID map
Coderunner app_kindShell does not load these apps
Workspace site end-user authSeparate from bootstrap — see 03-authentication.md

On this page