Skip to main content
fymo.yml lives at your project root and loads once, at startup. The class that handles it, ConfigManager, lives in fymo/core/config.py. The file itself is optional. Every section below has a sensible default, so a project with no fymo.yml at all still boots. It just runs with auth off and jobs on the in-process threaded provider. Each section loads through its own getter, like get_auth_config() or get_jobs_config(). An explicitly null section, auth: with nothing underneath, behaves the same as a missing one. Both resolve to {}.

Env var interpolation

${VAR} and ${VAR:-default} placeholders get resolved directly in the raw YAML text. This happens before yaml.safe_load ever parses it, and it happens once for the whole file, not per section. So a placeholder works the same whether it sits at the top level or three levels deep inside auth.providers.
  • ${VAR} is required. If VAR isn’t set, config loading raises ConfigurationError naming the variable. It never silently falls back to loading the literal string ${VAR}.
  • ${VAR:-default} falls back to the given default when VAR is unset. That fallback only evaluates when needed, so in ${A:-${B}}, B is never touched if A is already set.
  • An empty default (${VAR:-}) resolves to an empty string, not an error.
A default can contain literal braces. ${VAR:-{"a":1}} resolves to {"a":1}, because brace matching counts nesting depth instead of stopping at the first closing brace.An unterminated ${ raises ConfigurationError at load time. So does a malformed variable name like ${123bad}. Either way, the error names the problem.
Every resolved value gets spliced back into the YAML text as an explicit double-quoted scalar, never as a raw substring. So an env var can’t restructure your config, even if its value contains a newline followed by something that looks like a new YAML key, like admin: true. It always comes back out as the literal string it was. This is what makes interpolation safe to use with values you don’t fully control.

Typed values from strings

Every interpolated value is a plain YAML string. ${VAR} never becomes a real bool or int just because the variable holds text like “true” or “60”. That matters most for booleans: a bare bool("false") in Python evaluates to True, since any non-empty string is truthy. A config value like auth.enabled: ${AUTH_ENABLED} resolving to the string “false” would otherwise silently turn auth on. Fymo prevents this with parse_bool(), used for every boolean key in fymo.yml:
  • auth.enabled
  • remote.explicit_optin
  • security.headers.enabled
  • limits.rate_limit.enabled
  • limits.rate_limit.trust_proxy
Numeric keys like limits.max_body_bytes and limits.rate_limit.requests_per_minute don’t need this treatment. Plain int(...) already does the right thing here, since int("60") behaves exactly as expected.

Top-level keys

Your own code can read version and description back through ConfigManager.get(...) if it needs them. Nothing in the framework itself enforces or uses those two keys.

routes

limits

The rate limiter runs as a per-process, per-client-IP token bucket, one bucket per matched rule. There’s no sharing across worker processes. rate_limit.trust_proxy controls whether the first hop of X-Forwarded-For is trusted as the client IP. Only enable it behind a reverse proxy that actually overwrites that header, since otherwise a client could spoof its own rate-limit bucket. The same flag also decides whether X-Forwarded-Proto is honored when resolving the request’s scheme, which feeds both the HSTS header below and the session cookie’s Secure flag.

security

With headers.enabled on, every response gets four defaults automatically:
  • X-Content-Type-Options
  • X-Frame-Options
  • Referrer-Policy
  • Permissions-Policy
In production (dev=False), two more apply on top. Both are skipped entirely in dev:
  • Content-Security-Policy-Report-Only, with a default-src 'self' baseline. Added only if headers.extra doesn’t already set a CSP of either kind.
  • Strict-Transport-Security, added when the resolved request scheme is https. That resolution honors X-Forwarded-Proto only if limits.rate_limit.trust_proxy is on.
A header already present in the response from your own handler is never overwritten by these defaults.

auth

A provider entry can be a bare built-in name:
  • password
  • google
  • oidc
  • clerk
Or it can be an object: a type key for a built-in provider, or a class key for a dotted path to your own. Either way, it can carry whatever extra options that provider’s constructor takes. Add required: auto to an entry to skip it entirely when the provider isn’t configured, instead of crashing on a missing secret.

Authentication

Full provider config, the session cookie format, and how to write a custom UserStore or AuthProvider.

jobs

threaded runs jobs in-process, with no external dependency. A job is lost if the process restarts mid-run. procrastinate is a durable, Postgres-backed queue instead, meant to be picked up by a separate fymo jobs-worker process. Using it needs the procrastinate extra installed, plus a database, read from DATABASE_URL by default.

broadcasts

Same string-or-object shape as jobs.provider. postgres is currently the only built-in.

remote

With explicit_optin off (the default), every public, type-annotated top-level function in app/remote/*.py is exposed to the browser. Turning it on requires @remote on each function you want reachable. Everything else in that file stays server-only.

storage

Unlike jobs, broadcasts, and auth, there is no fallback provider here. Leaving storage: out of fymo.yml fails at startup if media: is configured. Silently writing to local disk is exactly the kind of default that works fine in dev, but quietly loses data in production.

Storage and Media

The StorageProvider interface, byte-range reads, and writing a custom provider.

media

media: is a list, defaults to []. Each entry becomes a byte-range-aware GET route, useful for video and audio scrubbing, with no hand-written WSGI handler needed. A prefix under the reserved /dist/ or /assets/ paths doesn’t fail the build, it only prints a warning. Those routes are matched first, so a prefix there would never actually get reached.

logging

An unrecognized value in any of these raises ValueError naming the key, failing fast at startup instead of silently logging somewhere unexpected.

Logging

What flows through fymo’s single root-logger handler, and the shape of request and job log lines.

The identity secret

FYMO_SECRET isn’t a fymo.yml key, it’s read straight from the environment. But it governs enough of the config above, like signed session cookies and OAuth’s PKCE state, that it deserves a mention here. Resolution order at startup:
  1. FYMO_SECRET env var, used as raw UTF-8 bytes. Must be at least 16 characters, or startup raises.
  2. .fymo/secret.key on disk, if present and at least 16 bytes.
  3. Dev mode only (dev=True or FYMO_DEV=1). 32 random bytes get generated and written to .fymo/secret.key (mode 0600) for reuse across restarts.
  4. Production with neither set: startup raises. A forgeable session cookie is a worse failure mode than refusing to boot.
FYMO_DEV and FYMO_SECRET are read with a simple truthy check, not with parse_bool: “1”, “true”, “yes”, or “on”, case-insensitive.That’s deliberate. Defaulting an unrecognized value to False here is a reasonable fallback for an env var read directly. The stricter parse_bool governs actual fymo.yml booleans instead, where a typo should raise rather than get guessed at.

Sections that aren’t read

Older or scaffolded projects sometimes carry a server: block (host, port, reload) and a build: block (output_dir, minify) in their fymo.yml. The framework doesn’t read either one. Host and port instead come from CLI flags:
If you see those sections in an older or generated fymo.yml, they’re inert: safe to leave, safe to delete.

Full example