foliot docs

Guides / Configuration

Configuration

The server is configured entirely through environment variables. Copy .env.example to .env for local work; pnpm dev:server loads .env without overriding anything already set in your environment.

Server

Variable Default Meaning
DATABASE_URL postgres://foliot:foliot@localhost:5432/foliot Postgres connection
NODE_ENV Set to production for a real deployment. It is what makes the settings under “Required in production” below mandatory: the server refuses to start without them
PORT, HOST 4100, 0.0.0.0 Listen address
LOG_LEVEL info debug, info, warn, error or silent
FOLIOT_OPERATOR_TOKENS token:name,token:name for Foliot Console and admin calls
FOLIOT_CORS_ORIGIN Comma-separated origins (or *) allowed to call the API from a browser. Unset means none
FOLIOT_HANDLER_EGRESS closed Where handlers may live. closed connects only to public addresses; open is for development (handlers on localhost) and is refused with NODE_ENV=production or a KMS key without FOLIOT_KMS_ENDPOINT. pnpm dev:server sets open for you
FOLIOT_HANDLER_ALLOW_CIDRS Private ranges handlers may live in under closed, 10.1.0.0/16,192.168.5.0/24. Opens them for every app. Link-local and metadata addresses are never reachable
FOLIOT_TRUST_PROXY 0 How many proxies sit in front, for resolving the client IP from X-Forwarded-For
FOLIOT_GIT_SHA The deployed commit, reported by /health. Falls back to RAILWAY_GIT_COMMIT_SHA, GIT_COMMIT_SHA, then SOURCE_VERSION

Required in production

With NODE_ENV=production the server refuses to start unless all of these are set, and its error lists every missing one at once with what would have quietly stopped working. Outside production they are optional (a development server has no Clerk), and a server that has Clerk sign-in on but lacks the key or the webhook secret logs a warning at boot saying what that costs.

Setting Without it
FOLIOT_CLERK_ISSUER Nobody can sign in (FOLIOT_CLERK_AUTHORIZED_PARTIES is required with it)
FOLIOT_CLERK_SECRET_KEY Invitations are recorded but never sent or matched, so nobody invited can join and an ownerless workspace cannot get an owner
FOLIOT_CLERK_WEBHOOK_SECRET Names and emails never sync, and a deleted Clerk account keeps its access
FOLIOT_KMS_KEY_ID App signing secrets would be wrapped with the development key

A forgotten NODE_ENV means none of this applies, so set it on every real deployment.

Scheduler and limits

Variable Default Meaning
FOLIOT_SCHEDULER_ENABLED true Run the claim loop in this process
FOLIOT_SCHEDULER_BATCH / _CONCURRENCY / _POLL_MS 50 / 32 / 250 Claim size, in-flight limit, idle poll
FOLIOT_LEASE_GRACE_MS 15000 How long a worker may hold a claimed run beyond the step timeout
FOLIOT_RETENTION_DAYS 90 Compact finished runs after this
FOLIOT_SYNC_TASK_MAX_MS 60000 Longest a synchronous handler call may block; longer steps should answer 202
FOLIOT_PAYLOAD_MAX_BYTES 262144 Cap on a run’s input, a signal payload, and each step’s output (over it fails the step, class output_too_large, and runs its onFailure). The total state is not capped, and neither is the result of an editRunState; each value of an edit’s patch is held to this limit, measured as a step’s output is, and each key to the same number of bytes

Signing secrets

Variable Default Meaning
FOLIOT_KMS_KEY_ID AWS KMS key that wraps app signing secrets. Required when NODE_ENV=production: the server refuses to start without it
AWS_REGION us-east-1 Region of that KMS key
FOLIOT_KMS_ENDPOINT LocalStack only: for example http://localhost:4566
FOLIOT_SIGNING_SECRET_ROLLOVER_MS 86400000 How long a rotated signing secret keeps verifying, so handlers can catch up
▶ Note.

Without NODE_ENV=production, a blank FOLIOT_KMS_KEY_ID is not an error: the server starts on the in-process development key wrapper. Values sealed by it do not open under KMS, so moving an existing database to KMS means rotating each app’s signing secret. A real deployment must set both NODE_ENV=production and FOLIOT_KMS_KEY_ID.

KMS calls use the standard AWS credential chain. Against LocalStack, any credentials work, but the AWS SDK refuses to start with none, so set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to anything.

Operator identity

Operator identity is a static token map: each token:name pair in FOLIOT_OPERATOR_TOKENS is one person, and every action they take is recorded against that name. A name may not begin with user: or app:: Foliot records those for a signed-in person and for an app’s own key, and refuses to start if an operator is named that way. Operator tokens and app API keys are unaffected by everything below.

An operator token must be an opaque random string. The server refuses to start if one has the shape of a JWT (three base64url parts, the first beginning eyJ), because that shape is how it tells a Clerk session token from every other credential.

Sign-in with Clerk

People sign in to Foliot Console through Clerk. The server verifies the Clerk session token the browser sends and learns who the person is, and nothing else: no organisation, role or metadata claim in the token is ever read to decide what they may do. That is decided from the server’s own tables.

Clerk is off by default. With no FOLIOT_CLERK_ISSUER, a Clerk token is just an unrecognised credential and is answered 401, exactly like any other.

Variable Default Meaning
FOLIOT_CLERK_ISSUER Your Clerk instance’s Frontend API URL, for example https://clerk.example.com: the exact iss a token must carry. No trailing slash or path. Setting it turns sign-in on
FOLIOT_CLERK_JWKS_URL <issuer>/.well-known/jwks.json Where that instance publishes its signing keys
FOLIOT_CLERK_AUTHORIZED_PARTIES Comma-separated origins a token may have been minted for (its azp), for example https://console.example.com,http://localhost:4101. Required when the issuer is set: a token for any other origin is refused even with a valid signature, and so is one with no azp at all

The three are checked together at boot. Setting the origins or the key URL without an issuer, or an issuer without origins, stops the server rather than leaving sign-in silently off or too permissive. URLs must be https (plain http is accepted only for localhost and 127.0.0.1).

The Clerk webhook

A second, independent variable turns on the webhook that keeps people’s names and emails in step with their Clerk accounts and removes a deleted account (see Workspaces, members and Clerk). It does not depend on the sign-in settings above, and either can be on without the other.

Variable Default Meaning
FOLIOT_CLERK_WEBHOOK_SECRET The Svix signing secret of the Clerk endpoint, whsec_ followed by base64, copied from the Clerk dashboard. Unset, POST /webhooks/clerk answers 404. A value that is not a well-formed secret stops the boot; the error names the variable and never repeats the value

A third variable lets Foliot call Clerk’s Backend API, so that invitations send email and a pending invitation can let a person in (see Managing members). Like the webhook, it is independent of sign-in.

Variable Default Meaning
FOLIOT_CLERK_SECRET_KEY The instance’s Backend API secret key, sk_ followed by letters and digits, from the API keys page of the Clerk dashboard. Unset, an invitation is recorded and nothing is sent or matched. A value that is not shaped like a secret key (the publishable pk_ key is the likely mistake) stops the boot; the error never repeats it
FOLIOT_CLERK_API_URL https://api.clerk.com Where the Backend API is reached. Only for a stand-in during development: it must be https, or http to a loopback host, because the secret key is sent to it. Setting it without the key stops the boot

The key is a secret. It lets the holder send email as your Clerk instance and read every user’s addresses, so it belongs in the server’s secret store and nowhere else, and it is never logged: printing the server’s configuration shows it as [redacted].

For a Clerk development instance such as https://your-instance.clerk.accounts.dev, the sign-in values are:

FOLIOT_CLERK_ISSUER=https://your-instance.clerk.accounts.dev
# Defaults to <issuer>/.well-known/jwks.json, which is
# https://your-instance.clerk.accounts.dev/.well-known/jwks.json
FOLIOT_CLERK_AUTHORIZED_PARTIES=http://localhost:4101

What the server checks on every token: an RS256 signature against the instance’s published keys (the algorithm is fixed, never read from the token), the issuer, the authorized party, expiry and not-before with five seconds of leeway, that the session is not pending, and that the subject is a Clerk user id.

  • Keys are fetched on first use and refreshed after an hour. The refresh happens behind a request that still holds a cached key, so one failed attempt at the hour mark signs nobody out: the server keeps verifying with its cached keys while Clerk’s endpoint is down, for up to six hours since the last good fetch, and then refuses with 503 (not 401, which would send a client back through sign-in) until the endpoint answers again. A key Clerk has revoked stops verifying at the first refresh that succeeds; during an outage it stays usable for at most those six hours. A token signed with an unknown key id triggers a refetch, at most one every 30 seconds however many such tokens arrive, so a flood of junk tokens cannot be turned into traffic to Clerk. After a rotation, a token signed with the new key can therefore be refused for up to 30 seconds. Each fetch has a 5-second timeout and a 64 KiB body cap.
  • A failed token is always the same 401 invalid token, whichever check failed; the reason is in the server log, never the token.
  • A verified person with no row in the users table gets one, with no workspace memberships, so they see nothing until someone adds them. Their email and name are copied from the token only if the session token template includes them.
  • What a person may do is decided on every request from their membership, read from the database each time and never cached: the app’s workspace, then their role in it. A viewer can read, an operator can also act on runs, workflows and step controls, and nothing about an app’s credentials or details is reachable by a person yet (those routes answer 403 whatever the role). An app in a workspace they are not in, an app with no workspace, and an app that does not exist all answer the same 404. An operator token and an app key are unaffected.