Skip to content

Apps declare their env and secrets in morca.toml (#331) - #400

Merged
winding-lines merged 4 commits into
stack-overlayfrom
app-env
Nov 12, 2026
Merged

winding-lines merged 4 commits into
stack-overlayfrom
app-env

Conversation

@winding-lines

@winding-lines winding-lines commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #331. Part of census#140.

Stacked on #399 (#332, overlays), which targets main. When #399 merges, this PR moves to main. It touches secrets handling, so it needs the user's review before merge, and auto-merge stays off.

What it does

An app declares what it needs in the project's morca.toml, the way a pipeline does. morca-stack builds the app's environment from that declaration instead of hardcoding it.

[apps.console]
port = 8080
check = "/healthz"
env = { CONSOLE_SEMANTIC = "1" }
lake = true                       # the warehouse settings and the stack's S3 keys
secrets = { POSTHOG_KEY = "orca/posthog-key" }
memory_gb = 1                     # docker --memory
  • morca (src/morca/apps_config.mojo):
    • [apps.<name>] is checked like every other table. Errors give file:line, the overlay's when an overlay set the value.
    • Names are lower-case letters, digits, _ and -, and can't be one of the stack's own containers.
    • env can't set PORT, the lake's variables or anything that looks like a secret, and every value must be one line.
    • Secret IDs are orca/... with no .. or //.
    • An overlay can change an app, not add one.
    • morca config apps [<file>] [--overlay F] needs no database. It prints one line per fact (console port 8080, console env A=1, console secret POSTHOG_KEY orca/posthog-key), with secrets by ID only. That's what the stack script reads. With no file it prints nothing.
  • morca-stack:
    • After the pull, plan and apply run config apps in each app's own image, with the overlay mounted. They then write /etc/orca/app-<name>.env (mode 600), one file per app, holding:
      • its env;
      • with lake = true, the lake: AWS_REGION, AWS_ENDPOINT_URL_S3, MORCA_WAREHOUSE, MORCA_PUBLIC_WAREHOUSE and s3.env;
      • each secret's value, read from /etc/orca/secrets/<id> (trailing newlines dropped, more than one line refused, never printed).
    • The port, check and --memory also come from the declaration. The old shared app.env is removed after the apply.
    • A secret the server doesn't have stops the deploy at the plan, before anything is paused.
    • Because the env file is part of the app's spec, changing a secret's value replaces the app, without downtime.
  • APPS=console in hetzner.env names which apps a stack runs, and nothing more. console:8080 still works: it gives the port to an app that has none in morca.toml, and a port that differs from the declared one is refused.
  • Secrets transport: SECRETS_FILE may now hold any secrets/orca/<id> key, which sync_settings sends the same way it sends the others, never printing a value. morca-stack sync accepts these paths using the same rules as secret-fetch (no ..). Only SECRETS_FILE carries app secrets: there's no per-file *_FILE setting for them.
  • Keeping today's census working: an app its image's morca.toml doesn't declare, or an image whose morca has no config apps (exit 2, before 0.25), gets the old fixed env (the lake, plus MORCA_POSTHOG_KEY as POSTHOG_KEY). The plan says so.
  • PostHog proxy: the UI now reads MORCA_POSTHOG_HOST (POSTHOG_HOST in hetzner.env) for where the PostHog snippet sends events. It can be a path on the UI's own origin (/ph) or an https:// origin. When it isn't set, the snippet talks to PostHog's own US hosts (us-assets.i.posthog.com, us.i.posthog.com), since there's no proxy. Any other value leaves the snippet out.

Design choices

  • Declaration format: the same TOML as [pipelines.<name>]. env and secrets are inline tables, secrets are IDs under orca/ as for pipelines, and lake is one boolean, not a list of variables. The stack reads the declaration through morca config apps from the image the app runs, so an app on its own tag gets its own declaration and the stack script doesn't parse TOML.
  • One env file per app: apps no longer share one file, so one app's secret never reaches another.
  • AWS is unchanged: morca aws's orca-app doesn't read [apps] yet.

Census side (its own PR, after morca-mojo 0.25.0)

Add this to morca.toml:

[apps.console]
port = 8080
check = "/healthz"   # or whatever path answers 200 once it's warmed up
lake = true
secrets = { POSTHOG_KEY = "orca/posthog-key" }

Add env only if the console reads something beyond these. Nothing else changes: the console still reads POSTHOG_KEY and PORT.

ops-secrets side (in the release window, before the census image with [apps.console])

  • In prod/secrets.sops.yaml (and dev's if dev has analytics), add secrets/orca/posthog-key: phc_..., the same project key as POSTHOG_KEY. Without it, a deploy of a census image that declares the secret stops at the plan.
  • In prod/hetzner.env, add POSTHOG_HOST=/ph, or the UI's analytics go straight to PostHog instead of through the mojourney-ph Worker.
  • Optionally, APPS=console, so the port comes from morca.toml. console:8080 keeps working.

Tests

Run locally on macOS:

  • test-config: the apps unit tests (keys, refusals, the config apps lines, an overlay can't add an app) and tests/config.sh (config apps with and without a file, with an overlay, and its count in config check).
  • test-ui: PostHog through MORCA_POSTHOG_HOST=/ph, PostHog's own host without it, and no snippet for a bad value.
  • tests/hetzner.sh, with only fake secret values:
    • the declared console's env, lake, secret, port, check, --memory and mode 600, with no value printed and no database;
    • an image without config apps gets the fixed env;
    • a missing secret stops at the plan, before any pause;
    • an APPS port that differs is refused, and APPS=console works;
    • SECRETS_FILE's secrets/orca/<id> keys are sent, and bad ones refused.
  • tests/hetzner-stacks.sh, tests/no-orca.sh and mojo format.

CI runs the rest.

🤖 Generated with Claude Code

https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6

winding-lines and others added 4 commits November 12, 2026 04:13
morca-stack wrote one fixed app.env for every app: the lake, the S3 keys
and the UI's PostHog key as POSTHOG_KEY. Now an app's [apps.<name>] says
what it gets (port, check, env, lake, secrets, memory_gb), read from the
app's own image with the new `morca config apps` (the overlay laid on
it), and each app has its own env file, app-<name>.env.

- Secrets: a secret's value comes from /etc/orca/secrets/<id>, never
  printed; SECRETS_FILE may hold any secrets/orca/<id> key, and sync
  takes them. A missing one stops the deploy at the plan.
- APPS=console names the app; a port there is for an app with none in
  morca.toml, and one that differs is refused.
- An undeclared app, or an image without `config apps`, keeps the old
  fixed env, so today's census images deploy as before.
- MORCA_POSTHOG_HOST (POSTHOG_HOST in hetzner.env): where the UI's
  PostHog snippet goes, a proxy like /ph; PostHog's own host without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6
@winding-lines
winding-lines marked this pull request as ready for review November 12, 2026 16:25
@winding-lines
winding-lines merged commit b8c81df into stack-overlay Nov 12, 2026
57 of 62 checks passed
@winding-lines
winding-lines deleted the app-env branch November 12, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant