Reference: app.manifest.yml¶
Every app has an app.manifest.yml at its root. This file is the app's self-description — the portal, gateway, and CI auto-discover apps from these manifests.
The schema is defined in schemas/app-manifest.schema.json (JSON Schema draft 2020-12) and validated in CI. For the rationale behind this design, see ../adr/0005-app-manifest-schema.md.
Full schema (v1)¶
# REQUIRED — Schema versioning
manifest_version: 1
# REQUIRED — Identity
id: # Stable UUID, generated by script. NEVER changes.
slug: client-invoice-tracker # kebab-case, unique repo-wide. Used in URLs, paths, DB names.
name: Client Invoice Tracker # Human-readable display name.
kind: product # 'product' | 'client' | 'internal' | 'platform'
description: > # One-sentence pitch for the app hub.
Track invoices for your clients and mark them as paid.
# REQUIRED — Visual identity in the portal
icon: receipt # Lucide icon name OR a path like 'assets/icon.svg'
color: '#0ea5e9' # Tailwind-friendly hex, used for tile background
# REQUIRED — Networking & routing
routing:
subdomain: invoices # → invoices.localhost in dev, invoices.${SCALR_PUBLIC_DOMAIN} in prod
ports:
frontend: 5174 # Local dev port (allocated by scaffold script)
backend: 8174 # Local dev port (allocated by scaffold script)
health_path: /health # Backend health endpoint (used by smoke test + gateway)
# REQUIRED — Authentication & entitlements
auth:
required: true # Almost always true. False only for public marketing pages.
authentik_app_slug: invoice-tracker # Slug of the corresponding Authentik Application.
required_groups: [] # Authentik group names. Empty = any authenticated user.
scopes: # OIDC scopes the app requests.
- openid
- profile
- email
# REQUIRED — Data ownership
database:
name: invoice_tracker_db # Postgres DB name (generated from slug by script).
migrations: alembic # Migration tool. 'alembic' is the only supported value today.
# OPTIONAL — Visibility in the portal app hub
hub:
visible: true # Show in app hub for entitled users? Default true.
category: finance # Grouping in the hub: 'finance' | 'ops' | 'tools' | 'admin' | etc.
order: 100 # Sort order within category. Lower = earlier.
# OPTIONAL — Inter-app dependencies
dependencies:
apis: [] # Other SCALR apps this one calls. e.g. ['billing', 'auth']
events_publish: [] # Events this app emits. e.g. ['invoice.paid']
events_subscribe: [] # Events this app reacts to. e.g. ['user.deleted']
# OPTIONAL — Operational metadata
ownership:
team: platform # Owning team / individual.
contact: nickolai@scalr.com # Where to route bugs / questions.
lifecycle:
status: active # 'active' | 'beta' | 'deprecated' | 'archived'
created: 2026-04-29 # ISO date — set by scaffold script.
# OPTIONAL — Build / deploy hints
build:
frontend:
dockerfile: frontend/Dockerfile
context: frontend
backend:
dockerfile: backend/Dockerfile
context: backend
# OPTIONAL — Resource hints (advisory; for future orchestrator use)
resources:
frontend:
memory: 256Mi
cpu: 100m
backend:
memory: 512Mi
cpu: 250m
Field-by-field reference¶
Identity¶
manifest_version (required) — Integer. Currently 1. Lets us evolve the schema without breaking old apps.
id (required) — UUID. Stable, never changes, even if the app is renamed. Entitlements, audit logs, and cross-app references key off this.
slug (required) — kebab-case, unique repo-wide. Used in URLs, paths, DB names. If the app is renamed, the slug and directory change but the id stays.
name (required) — Human-readable display name shown in the portal.
kind (required) — One of product | client | internal | platform. Determines where the app lives in the tree:
- product → apps/products/<slug>/
- client → clients/<slug>/
- internal → apps/admin/ or similar
- platform → services/<slug>/
description (required) — One sentence for the app hub tile.
Visual identity¶
icon (required) — Either a Lucide icon name (receipt, users, chart-line) or a path to an SVG asset (assets/icon.svg).
color (required) — Hex code for the accent color of the app's tile in the hub.
Routing¶
routing.subdomain (required) — Subdomain prefix. The gateway derives the full domain (<subdomain>.scalr.com).
routing.ports.frontend / routing.ports.backend (required) — Local dev ports. Allocated by the scaffold script. Humans rarely touch these.
routing.health_path (required) — Backend health endpoint. Used by the smoke test and the gateway.
Auth¶
auth.required (required) — Boolean. Almost always true. false only for public marketing pages.
auth.authentik_app_slug (required) — Slug of the corresponding Authentik Application. The portal uses this to check entitlements.
auth.required_groups (required) — Array of Authentik group names. Empty array means any authenticated user.
auth.scopes (required) — OIDC scopes the app requests. Default [openid, profile, email].
Database¶
database.name (required) — Postgres DB name. Generated from slug by the scaffold script.
database.migrations (required) — Migration tool. alembic is the only supported value today.
Hub (optional)¶
hub.visible — Default true. Set to false for apps that exist but shouldn't appear in the hub.
hub.category — Grouping label in the hub.
hub.order — Sort order within category. Lower = earlier.
Dependencies (optional)¶
dependencies.apis — Other SCALR apps this one calls. Used for impact analysis.
dependencies.events_publish — Events this app emits. Captured even before a message bus exists.
dependencies.events_subscribe — Events this app reacts to.
Ownership (optional)¶
ownership.team — Owning team or individual.
ownership.contact — Email or handle for bugs/questions.
Lifecycle (optional)¶
lifecycle.status — One of active | beta | deprecated | archived. The portal uses this to show banners or hide apps.
lifecycle.created — ISO date. Set by scaffold script.
Build (optional)¶
build.frontend.dockerfile / build.frontend.context — Where the frontend Dockerfile lives. Defaults to frontend/Dockerfile and frontend.
build.backend.dockerfile / build.backend.context — Same for backend.
Resources (optional, advisory)¶
resources.frontend.memory / resources.frontend.cpu — Currently advisory. When we eventually move from compose to Kubernetes/Nomad, these become real limits.
Validation¶
infra/scripts/validate-manifest.sh <path> validates a single manifest against the schema. It's invoked by:
- The scaffold script after generation (sanity check).
- The smoke test (asserts the manifest is valid).
- CI on every PR (against every manifest in the repo).
Invalid manifests fail the build. No silent fallbacks.
Auto-discovery¶
The portal builds the app hub at runtime by:
- Scanning
apps/**/app.manifest.ymlandclients/**/app.manifest.yml. - Filtering by
lifecycle.status != "archived"andhub.visible == true. - Querying Authentik for the current user's entitled application slugs.
- Intersecting: only apps where
auth.authentik_app_slugmatches an entitled app are shown. - Grouping by
hub.category, sorting byhub.order.
Adding a new app to the hub = creating its manifest. No portal code changes.
Example: minimal manifest after scaffolding¶
What create-app-from-template.sh produces — fully valid, ready to boot, ready for customisation:
manifest_version: 1
id: 7f3c9a2e-4b1d-4e8a-9c2f-1d3e5b7a9c4f
slug: invoice-tracker
name: Invoice Tracker
kind: product
description: TODO — describe what this app does in one sentence.
icon: square
color: '#64748b'
routing:
subdomain: invoice-tracker
ports:
frontend: 5174
backend: 8174
health_path: /health
auth:
required: true
authentik_app_slug: invoice-tracker
required_groups: []
scopes: [openid, profile, email]
database:
name: invoice_tracker_db
migrations: alembic
hub:
visible: true
category: tools
order: 100
ownership:
team: platform
contact: nickolai@scalr.com
lifecycle:
status: active
created: 2026-04-29