Skip to content
Serelane

Configuration

Serelane is configured entirely through the environment, so a container needs no configuration file and nothing mounted to hold one.

Every setting is validated at boot, and every problem is reported at once rather than one restart at a time. A container with no environment at all boots on the defaults below.

Core

VariableDefaultWhat it does
SERELANE_BASE_URLderived from the Host headerThe absolute URL this instance is reachable at. It goes into emails and link previews, which a relative address cannot serve. Must be an absolute http or https URL, and a trailing slash is trimmed. Behind a proxy that does not set Host, the guess is wrong, so set this.
SERELANE_DATA_DIR./dataWhere the database, the uploads and the generated secret key live. The only path Serelane writes to. The container image sets this to /data.
SERELANE_LOG_LEVELinfoOne of debug, info, warn or error.
SERELANE_PORT8080The TCP port to listen on. Between 1 and 65535.
SERELANE_SECRET_KEYgenerated into the data directorySigns sessions, sign-in links and anonymous cookies. At least 32 characters. Left unset, one is generated into secret.key and the instance says so loudly at boot: back that file up, because losing it signs everyone out and invalidates every pending sign-in link.
SERELANE_TRUSTED_PROXIESemptyComma-separated CIDR blocks, or bare addresses for a single host. Only these networks’ X-Forwarded-For is believed. Unset means the header is ignored entirely, so every visitor appears to come from the proxy, which collapses anonymous vote deduplication and rate limiting onto one address.

Database

VariableDefaultWhat it does
SERELANE_DB_DRIVERsqliteThe only accepted value today. Postgres is a planned driver, not a shipped one, and any other value fails at boot.
SERELANE_DB_DSNserelane.db inside the data directoryWhere the database lives. Defaulting inside the data directory is what makes moving that directory move the database with it.

Mail

Mail is optional. With no SERELANE_SMTP_HOST, sign-in links and notifications are written to the log instead of sent, so magic-link identity does not work and nobody is notified of anything.

VariableDefaultWhat it does
SERELANE_SMTP_FROMnoneThe envelope sender. Required once SERELANE_SMTP_HOST is set; the instance refuses to boot without it.
SERELANE_SMTP_HOSTemptyThe relay. Setting it is what turns mail on.
SERELANE_SMTP_PASSWORDemptyThe password for the relay.
SERELANE_SMTP_PORT587The relay’s port.
SERELANE_SMTP_TLSstarttlsOne of starttls (port 587, a plaintext session upgraded), implicit (port 465, TLS from the first byte) or none (in the clear, which is only ever right for a relay on localhost). true, 1 and yes are accepted as synonyms for starttls, and false, 0 and no for none, because this setting used to be a boolean.
SERELANE_SMTP_USERNAMEemptyThe username for the relay.

Spam and abuse

The challenge is instance configuration because the keys are per deployment and because it decides whether the process can verify a solution at all. Which boards actually use it is a per-project setting, in the database.

VariableDefaultWhat it does
SERELANE_CAPTCHA_PROVIDERemptyaltcha, turnstile, or empty for none. Altcha is a self-hosted proof of work and involves no third party, which is why it is the one to reach for first on a self-hosted instance.
SERELANE_CAPTCHA_SECRETthe instance secret keySigns altcha challenges and authenticates to Turnstile. Required for turnstile. In altcha mode it falls back to SERELANE_SECRET_KEY, so the privacy-preserving option needs no configuration beyond switching it on.
SERELANE_CAPTCHA_SITE_KEYemptyThe public half a third-party widget renders with. Required for turnstile.

Sign-in

Both providers reject an explicit email_verified: false assertion. The console requires the ID token to assert email_verified: true, while a board also accepts an omitted optional claim after retrieving the address from subject-matched UserInfo. A provider that omits the claim entirely, which Azure AD and SAML-bridged identity providers do, is refused exactly as if it had sent false — omitting an assertion is not making one. This applies to the admin console provider and the board provider alike.

Admin console

Configures the OIDC provider the operator console signs in through. This is separate from the board provider below: setting one never configures the other, so an instance can require a second factor of its operators without requiring one of every visitor who votes on a roadmap. The client secret is optional here too, since every authorisation request carries a PKCE S256 challenge. Register <base URL>/api/v1/admin/auth/callback as the redirect URI with the provider, using the instance’s own base URL in place of <base URL>. This is a different path from the board provider’s callback below, so a provider can tell the two sign-ins apart.

VariableDefaultWhat it does
SERELANE_ADMIN_OIDC_CLIENT_IDemptyThe application registered with the provider. Required once SERELANE_ADMIN_OIDC_ISSUER is set.
SERELANE_ADMIN_OIDC_CLIENT_SECRETemptyOptional. Every authorisation request carries a PKCE S256 challenge, so a public client is a complete configuration on its own.
SERELANE_ADMIN_OIDC_ISSUERemptyThe provider’s base URL, the one its discovery document hangs off. Required once SERELANE_ADMIN_OIDC_CLIENT_ID is set.
SERELANE_ADMIN_OIDC_LABELemptyWhat the sign-in button says.
SERELANE_ADMIN_OIDC_ROLEemptyA role the provider has to assert before an admin sign-in is allowed. Empty leaves the check off, which is the behaviour of every earlier release.
SERELANE_ADMIN_OIDC_ROLE_CLAIMrolesWhich claim carries the roles. A dotted path walks into a nested claim, such as realm_access.roles. Only read once SERELANE_ADMIN_OIDC_ROLE is set.
SERELANE_ADMIN_OIDC_ROLE_ORGemptyNarrows the grant to one organisation, named by its id or by its primary domain, for providers whose role claim records where each role was granted.
SERELANE_ADMIN_OIDC_SCOPESemptySpace-separated. Empty means the default set.

Requiring a role

Without SERELANE_ADMIN_OIDC_ROLE, any account on the instance whose address the provider has verified can sign in to the console. That is enough when the provider only knows operators. It is not enough when one provider serves the boards and the console, because everyone who can sign up can then reach the console door with an address that might already have an account behind it.

Naming a role closes that door. The role permits an admin sign-in and does nothing else: it never creates an account, never grants is_superadmin, and never changes a project membership. Both checks have to pass, so an operator still needs the role at the provider and an existing account on this instance whose address the provider has verified.

Nothing in OpenID Connect says where roles live, so the claim is named rather than guessed. Three shapes are understood, which between them cover what providers send:

{ "roles": ["admin", "viewer"] }
{ "roles": "admin viewer" }
{ "roles": { "admin": { "178": "acme.example" } } }

The third is what providers with a tenant model send, mapping each role to the organisations it was granted in. SERELANE_ADMIN_OIDC_ROLE_ORG matches either half of that pair, so 178 and acme.example both name the same organisation.

Role names are matched exactly, so Admin does not satisfy a requirement for admin.

The ID token is read first. A token that carries the claim is the provider’s answer, whatever it says. A token that does not carry the claim at all is a provider that keeps roles at its UserInfo endpoint, which is asked next, and whose response is only believed when it is about the same subject. Every question that cannot be answered is answered no: a missing claim, a claim in a shape nothing understands, an unreachable UserInfo endpoint, and an organisation a list-shaped claim cannot express all refuse the sign-in.

A refused sign-in tells the browser the same generic thing every other refusal does. The specific reason goes to the process log and the audit trail, which is where to look when a role that was granted is not being seen.

Some providers only put roles in a token when the scope asks for them. SERELANE_ADMIN_OIDC_SCOPES is how that scope is added, and it replaces the default set rather than extending it, so list openid and email alongside whatever the provider needs.

Two worked examples:

# Keycloak, whose realm roles are nested
SERELANE_ADMIN_OIDC_ROLE=admin
SERELANE_ADMIN_OIDC_ROLE_CLAIM=realm_access.roles
# ZITADEL, whose roles are scoped to a project and an organisation
SERELANE_ADMIN_OIDC_ROLE=admin
SERELANE_ADMIN_OIDC_ROLE_CLAIM=urn:zitadel:iam:org:project:roles
SERELANE_ADMIN_OIDC_ROLE_ORG=acme.example
SERELANE_ADMIN_OIDC_SCOPES="openid email urn:zitadel:iam:org:project:id:271828:aud"

The ZITADEL example scopes the token to project 271828 so that project’s roles are the ones asserted. A deployment that would rather pin the project in the claim name can set SERELANE_ADMIN_OIDC_ROLE_CLAIM to urn:zitadel:iam:org:project:271828:roles instead, which refuses a role held in any other project even if the token carries several. Check the claim your provider actually emits before turning the requirement on, by reading a decoded ID token or the UserInfo response for an operator who already has the role.

Signing in to the console through a provider covers what the requirement means for sessions that already exist, and how to move a deployment onto it.

Board provider

Configures one OIDC provider for every board on the instance. It is optional, and when it is set it replaces whatever a project has stored, because a deployment that names its provider here should not have that quietly overridden from a form. Register <base URL>/api/v1/public/auth/callback as the redirect URI with the provider, using the instance’s own base URL in place of <base URL>.

Identities are joined on the verified email address, not on the provider. Pointing an instance at a different provider lets anyone who controls that same address there sign in as the existing identity. They inherit its votes, comments and subscriptions. Only repoint SERELANE_OIDC_ISSUER at a provider you trust to guard the same addresses as the one it replaces.

VariableDefaultWhat it does
SERELANE_OIDC_CLIENT_IDemptyThe application registered with the provider. Required once SERELANE_OIDC_ISSUER is set.
SERELANE_OIDC_CLIENT_SECRETemptyOptional. Serelane always sends a PKCE challenge, so a public client is a complete configuration on its own, and some providers will not issue a secret for one.
SERELANE_OIDC_ISSUERemptyThe provider’s base URL, the one its discovery document hangs off. Required once SERELANE_OIDC_CLIENT_ID is set.
SERELANE_OIDC_LABELemptyWhat the sign-in button says.
SERELANE_OIDC_SCOPESemptySpace-separated. Empty means the default set.

First run

These are read on every invocation but only used by serelane bootstrap, which creates the first admin account without a browser. They live here rather than in flags so that an operator provisioning from a compose file finds them beside every other setting.

VariableDefaultWhat it does
SERELANE_BOOTSTRAP_DEMOon for a dev instance, otherwise offFills the example board with content.
SERELANE_BOOTSTRAP_EMAILnoneThe first admin account’s address. Must contain an @.
SERELANE_BOOTSTRAP_NAMEnoneThe display name on that account.
SERELANE_BOOTSTRAP_PASSWORDnoneThat account’s password. Checked for length up front, so a typo fails the command rather than creating an account nobody can sign in to. Not trimmed: leading and trailing spaces are a legitimate part of a password, and silently changing what you set would lock you out.

Without both an email and a password, serelane bootstrap refuses rather than inventing credentials. Create the account through the setup form instead, at /setup.

Development

VariableDefaultWhat it does
SERELANE_DEVoffSet to 1 to serve the frontend by proxying to the Vite dev server instead of from the embedded copy, and to allow serelane bootstrap to fall back to built-in credentials. Never set this on an instance anyone else can reach.