You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The backend is configured through TOML files listed in the ACADEMY_CONFIG environment variable (see ARCHITECTURE.md).
config.toml in the repository root is always loaded first and holds the defaults; every file named in ACADEMY_CONFIG overrides it, files listed earlier taking priority over later ones.
Run academy check-config --verbose to validate the resulting configuration and print it.
This page lists every property the backend reads.
Properties marked required have no default and must be set by the deployment.
Durations are strings built from d, h, m and s parts, e.g. "30d", "10m" or "1h 30m".
[http]
Property
Default
Description
address
required
Socket address the API server binds to, e.g. "0.0.0.0:80".
real_ip.header
unset
Header to read the client ip from when running behind a reverse proxy, e.g. "X-Real-Ip". Without it the socket address of the TCP client is used.
real_ip.set_from
required with real_ip.header
Address of the reverse proxy. The header is read only when the request comes from it; requests from anywhere else keep their socket address, so a client cannot choose its own ip.
allowed_origins
[]
List of regular expressions matching the origins that are allowed by CORS.
Sender of all outgoing emails, e.g. "Bootstrap Academy <noreply@example.com>".
[jwt]
Property
Default
Description
secret
required
Secret used to sign and verify JSON Web Tokens.
download_token_ttl
"10m"
Lifetime of the tokens issued for file downloads.
[internal]
Property
Default
Description
jwt_ttl
"10s"
Lifetime of the tokens the backend issues to authenticate itself against the microservices.
[internal.secrets]
One secret per audience of the internal service tokens, so that a token issued for one service cannot be used against another.
An audience that is not listed here falls back to jwt.secret, on signing and on verification alike, which keeps the shared secret in use until a dedicated one has been deployed to every sender and to the receiver.
The audiences are auth and shop (incoming, verified by the _internal endpoints of this backend) and skills, challenges and events (outgoing, expected by the microservices).
academy jwt sign signs with the secret of the audience named in the payload's aud, so a break-glass token minted on the command line is accepted by the endpoints of that audience.
Property
Default
Description
<audience>
unset
Secret used to sign and verify the internal service tokens of that audience.
[health]
Property
Default
Description
database_cache_ttl
"10s"
How long a database health check result is reused.
cache_cache_ttl
"10s"
How long a cache health check result is reused.
email_cache_ttl
"10s"
How long an email health check result is reused.
[user]
Property
Default
Description
terms_version
"2026-09"
Version of the terms and conditions that is currently in force. POST /auth/users and POST /auth/users/{user_id}/terms accept only this version and record it; any other value is rejected with 422. Has to match TERMS_VERSION in the frontend, which decides when the acceptance is asked for.
name_change_rate_limit
"30d"
Minimum time between two changes of a user name.
export_rate_limit
"10m"
Minimum time between two data exports (GET /auth/users/{user_id}/export) of the same user. Administrators are exempt.
verification_code_ttl
"4h"
Lifetime of an email verification code.
verification_redirect_url
"https://bootstrap.academy/auth/verify-account"
Target of the link in the verification email.
password_reset_code_ttl
"4h"
Lifetime of a password reset code.
password_reset_redirect_url
"https://bootstrap.academy/auth/reset-password"
Target of the link in the password reset email.
code_email_cooldown
"1m"
Minimum time between two emails with a code of the same kind to the same address. A verification applies it per account and mailbox, so two accounts may each get a code to the same mailbox within one minute, and an account that corrects a mistyped address gets a code to the corrected one at once. A password reset applies it per address exactly as requested.
code_email_window
"1h"
Window in which the emails with a code are counted. It starts with the first email counted in it and is not extended by the ones that follow.
code_emails_per_account
5
Maximum number of emails with a code of the same kind for one account within the window. At least 1.
code_emails_per_address
5
Maximum number of emails with a code of the same kind to one address within the window. At least 1. A verification counts it per account and mailbox: ASCII case, a +tag in an unquoted local part and, for Gmail, dots and the googlemail.com domain do not make a separate address. A password reset counts it per address exactly as requested, without any folding.
code_emails_per_mailbox
20
Maximum number of verification emails to one mailbox within the window, across all accounts. Has to be at least twice the smaller of code_emails_per_address and code_emails_per_account: one account can put up to twice that budget minus one into one window of the mailbox (9 with the defaults), because its own windows can lie across the mailbox's. Loading the config checks the rule, so one account alone cannot lock anybody out of the confirmation.
Anybody can point the address of an account, or the address named in a password reset, at somebody else's mailbox. The last five properties therefore limit POST /auth/users/{user_id}/email and POST /auth/password_reset, each kind of code on its own, in buckets in the cache. A bucket that refuses a request does not count it, and the request is answered with 429 and a Retry-After header. A bucket checked earlier keeps its count when a later one refuses, which is why the buckets of the requester come first.
A verification first looks at the mailbox across all accounts without counting; once its shared budget is used up, the request is refused before anything is counted. It is then checked against the account together with the mailbox (cooldown and code_emails_per_address), the account (code_emails_per_account) and the mailbox across all accounts (code_emails_per_mailbox). A refusal therefore only costs the requesting account, and only emails that are actually sent fill the shared bucket. Only a request that races others for the last place in the shared budget can be refused by it after counting in its own buckets.
A password reset counts the address exactly as requested, before it is looked up and whether or not an account has it, and then the account it found. The code goes out only when the requested address equals the stored one exactly, so any other spelling (other case, a tag, dots) sends nothing and leaves the owner's bucket alone. The answer of the limit does not tell whether an account has the address, with one exception: an account that changed its address within the window may run out of its own budget before its new address does. Whether an address is taken can be learned elsewhere anyway, for example from POST /auth/users.
A refusal leaves the codes that were already sent valid, so whoever is refused can still use the code they received.
[session]
Property
Default
Description
access_token_ttl
"5m"
Lifetime of an access token.
refresh_token_ttl
"30d"
Lifetime of a refresh token. Sessions that have not been refreshed within this period are removed by academy task prune-database.
refresh_token_length
64
Length of the generated refresh tokens in bytes.
login_fails_before_captcha
3
Number of failed logins after which a captcha is requested. Only relevant while reCAPTCHA is enabled.
login_fails_before_lock
5
Number of failed attempts against one login after which it is locked. Counted per login (the user name and the email address of the same account share the count) in the cache, independently of the captcha, so password guessing is slowed down even while reCAPTCHA is switched off. A successful login clears the count.
login_fail_window
"15m"
Lifetime of the counter of failed attempts against one login. It starts again with every counted attempt, and never expires before the lock it caused has run out.
login_lock_initial
"1m"
Length of the first lock of a login. Every following failed attempt locks it for twice as long as the one before.
login_lock_max
"15m"
Longest lock a login can receive.
login_fails_per_ip
30
Number of failed attempts from one client ip address after which further attempts from it are refused for login_ip_window. Only failures are counted, so a household, a school or a carrier-grade NAT full of people logging in successfully never reaches it; it exists to stop one machine from working through a list of accounts.
login_ip_window
"15m"
Lifetime of the counter of failed attempts from one client ip address, and the length of the block that follows it. Requires http.real_ip to be configured when the api runs behind a reverse proxy, or every request lands in the bucket of the proxy.
[totp]
Property
Default
Description
secret_length
32
Length of generated TOTP secrets in bytes.
[contact]
Property
Default
Description
email
required
Recipient of the contact form and of the internal notifications about contract declarations.
[contract]
Rate limits of the consumer declaration endpoints (POST /contracts/cancellations and POST /contracts/withdrawals), counted per client ip address and per email address over the same window.
Both counters are shared between the two endpoints, and a request that is refused is not counted.
The per-ip budget only stops a single machine from flooding the endpoint, so it is deliberately much larger than the per-address one: everybody behind the same NAT — a household, a school, a carrier's CGNAT — shares one ip address, and § 312k Abs. 2 BGB requires the cancellation form to be permanently available to each of them.
Property
Default
Description
rate_limit_window
"1h"
Lifetime of a counter. It starts again with every counted request, so the window slides rather than being reset at a fixed point.
rate_limit_per_ip
60
Maximum number of declarations per client ip address within the window.
rate_limit_per_email
5
Maximum number of declarations per email address within the window.
retention_years
3
Number of years a cancellation or withdrawal declaration is kept as evidence, counted from the end of the calendar year in which it was received. Three years is the regular limitation period (§ 195 BGB) counted the way § 199 Abs. 1 BGB counts it. Enforced by academy task prune-database.
[recaptcha]
The section is always parsed, so sitekey and secret have to be set even when the check is switched off.
Property
Default
Description
enable
true
Set to false to drop the whole section: no reCAPTCHA response is verified and GET /auth/recaptcha reports no site key, so the frontend does not load the reCAPTCHA script.
siteverify_endpoint_override
unset
Alternative siteverify endpoint, used by the test setup.
sitekey
required
reCAPTCHA site key, published through GET /auth/recaptcha.
secret
required
reCAPTCHA secret key.
min_score
0.5
Minimum score a reCAPTCHA response has to reach.
[vat]
Property
Default
Description
validate_endpoint_override
unset
Alternative VIES endpoint for VAT id validation, used by the test setup.
[paypal]
Property
Default
Description
base_url_override
unset
Alternative PayPal API base url, used by the test setup.
client_id
required
PayPal client id, published through GET /shop/coins/paypal.
client_secret
required
PayPal client secret.
[coin]
Property
Default
Description
purchase_min
500
Smallest number of Morphcoins that can be bought in one order.
purchase_max
1000000
Largest number of Morphcoins that can be bought in one order.
[heart]
Property
Default
Description
max
6
Maximum number of hearts a user can hold.
refill_price
50
Price of a heart refill in Morphcoins.
auto_refill_time
"00:00"
Time of day in UTC at which hearts are refilled automatically.
[premium]
Property
Default
Description
monthly_price
1000
Price of one month of premium in Morphcoins. Automatic renewals always use this plan.
yearly_price
10000
Price of one year of premium in Morphcoins.
[render]
Property
Default
Description
daemon_url
required
Base url of the render daemon (academy_render_daemon), which renders HTML to PDF.
timeout
"30s"
Timeout of a single render request. A daemon that accepts the connection and then stops answering would otherwise keep the waiting request open indefinitely.
[microservices]
Base urls of the microservices the backend calls over the internal API, to propagate account deletions (see ARCHITECTURE.md) and to collect the data export of a user (see ARCHITECTURE.md).
A microservice without a url is skipped; an empty string counts as no url.
Skipped means silently skipped: account deletions are not propagated to it, and its share of a data export is simply absent — the export does not list it as unavailable and still reports itself as complete, because "no url" means "this service is not part of this deployment".
Which services are configured and which are not is logged when the backend starts.
Timeout of a single export request, which reads more data than a deletion.
max_export_size
33554432
Maximum size in bytes of the export response of a single microservice. A larger response is not buffered; the service is reported as unavailable in the export instead.
[finance]
Property
Default
Description
vat_percent
19
VAT rate in percent, published through GET /shop/coins/config. Has to be at least 0 and less than 100; the backend refuses to start otherwise, because the net amounts of an invoice are derived from the gross amount that was paid.
invoices_archive
required
Directory the generated invoices are written to.
credit_notes_archive
required
Directory the generated credit notes are written to.
final_statements_archive
required
Directory the final statements issued at account deletion are written to.
retention_years
8
Number of years invoices, credit notes and final statements are kept, counted from the end of the calendar year in which they were issued. Enforced by academy task prune-documents.
[sentry]
Optional section for error reporting to GlitchTip/Sentry. It is not present in config.toml, so error reporting is off unless the deployment adds it.
Property
Default
Description
enable
true
Set to false to drop the whole section.
dsn
required
DSN of the GlitchTip/Sentry project.
[oauth2]
Property
Default
Description
enable
true
Set to false to disable OAuth2 entirely. OAuth2 is also disabled if no provider remains enabled.
registration_token_ttl
"10m"
Lifetime of the token issued after an OAuth2 login without a linked account.
authorization_ttl
"10m"
How long a started authorization flow (POST /auth/oauth/authorize) can be completed. It only has to cover the round trip through the provider's consent screen.
redirect_uris
["https://bootstrap.academy/oauth/callback"]
The redirect uris POST /auth/oauth/authorize accepts, compared exactly; anything else is rejected with 400 Redirect uri not allowed. The uri ends up in the authorize url the backend hands out and is used again for the token exchange, so a deployment reached under another domain has to list its own callback here or OAuth2 stops working for it.
[oauth2.providers.<id>]
config.toml predefines github, discord and google with everything except the credentials; further providers can be added under any id.
Every provider is parsed before the disabled ones are dropped, so client_id and client_secret have to be set even for a provider with enable = false — an empty string is enough.
Property
Default
Description
enable
true
Set to false to remove the provider.
name
required
Display name of the provider.
client_id
required
OAuth2 client id. Required even when the provider is disabled.
client_secret
required
OAuth2 client secret. Required even when the provider is disabled.
pkce
true
Whether the provider supports PKCE (RFC 7636). Set to false only for a provider that rejects code_challenge.
auth_url
required
Authorization endpoint.
token_url
required
Token endpoint.
userinfo_url
required
Endpoint returning the profile of the authenticated user.
userinfo_id_key
required
Key of the user id in the userinfo response.
userinfo_name_key
required
Key of the user name in the userinfo response.
scopes
required
Scopes to request, e.g. ["identify"].
Prospective purchase confirmation and activation
purchase.provision_window_seconds maps premium_monthly, premium_yearly,
hearts, course and coins to a positive integer number of seconds. There is
no default commercial promise. A missing entry prevents a new immediate-product
offer; it does not disable recovery or remove previously accepted rights.
Before activation the operator must approve a supportable duration for each
enabled product and assign the overdue-claim handling. Do not copy synthetic test
durations into a release configuration. The exact duration is saved and disclosed
in the offer; its clock starts at the saved binding acceptance, including PayPal
orders. A later payment does not restart it. Scheduled Events use their accepted
start as the source-availability cutoff.
Overdue unperformed orders stop automatic provision and remain visible for review.
No new provider capture starts after the saved window. An existing or uncertain
provider capture still has to be reconciled under its original identity. If a
provision transaction actually committed but timely visibility is unproved, keep
the real provision and claim with separate timing evidence; never invent an exact
COMMIT time, retroactively erase access, or report a refund that did not occur.
The purchase replay worker must remain scheduled. This configuration supplies no
refund/tender/forfeiture policy and no retention or staff-response promise.