Tiers & Configuration¶
Revel is configured almost entirely through environment variables in .env and the set of Docker Compose profiles you enable. This page documents the two reference tiers and every knob you can turn.
Slim vs Full¶
The two reference sizings differ in which optional profiles run and how generous the resource limits are:
| Setting | Slim (~2 vCPU / 4 GB) | Full (8 vCPU / 32 GB) |
|---|---|---|
COMPOSE_PROFILES (wizard default) | (empty) | observability,antivirus |
Postgres shared_buffers | 256MB | 4GB |
| Gunicorn workers | 2 | 6 |
| Celery concurrency | 2 | 4 |
Slim runs only the core services (caddy, frontend, web, celery, beat, postgres, pgbouncer, redis). Full adds antivirus scanning and the LGTM observability stack by default; the Telegram bot and the login canary stay optional. The tier is only a preset: either tier can run any of the profiles below, and the setup wizard asks about each one.
Compose profiles¶
Each optional capability is gated behind a Compose profile. Add the profile name to COMPOSE_PROFILES (comma-separated) to run it:
- observability — Prometheus, Loki, Tempo, Pyroscope, Alloy, and Grafana. See Observability.
- antivirus — the ClamAV daemon used by
FEATURE_MALWARE_SCAN. - telegram — the Telegram bot process.
- canary — the synthetic-monitoring canary.
If you leave COMPOSE_PROFILES empty you get the Slim core. Remember to pair each profile with its feature flag — for example, running the antivirus profile but leaving FEATURE_MALWARE_SCAN=False just wastes RAM.
Environment knobs¶
Compose & resources¶
COMPOSE_PROFILES— comma-separated list of optional profiles to enable (see above).PG_SHARED_BUFFERS,PG_*— PostgreSQL tuning (e.g.shared_buffers, work_mem). Slim uses conservative values; Full scales them up.- Resource-limit vars — per-service CPU/memory limits (Gunicorn worker count, Celery concurrency, container memory ceilings) used to fit the stack onto small or large hosts.
Domains¶
FRONTEND_DOMAIN— public hostname of the web app.API_DOMAIN— public hostname of the API.GRAFANA_DOMAIN— public hostname of Grafana, needed whenever theobservabilityprofile is enabled, on either tier.BASE_URL— required in production. The API's public HTTPS origin (e.g.https://api.example.org). It builds absolute backend links in emails, including theList-Unsubscribeone-click URL. The default,http://localhost:8000, only suits local development.FRONTEND_BASE_URL— required in production. The web app's public origin, used for links into the app.SiteSettings.frontend_base_url(admin → Site settings) is seeded once from it when the row is first created, so if you changeFRONTEND_BASE_URLlater, update that field too.
The setup wizard writes both from your domains. With DEBUG off, manage.py check validates them at startup. The web container runs migrate on start, which runs the same checks, and so does the telegram container's run_telegram_bot (Celery workers and beat don't):
| Check | Level | When |
|---|---|---|
common.E001 | Error: check/migrate fail, the API doesn't start | BASE_URL is empty, has no scheme, points at localhost or any loopback address, or is still the old default demo.letsrevel.io |
common.W002 | Warning | BASE_URL is not https:// |
common.W003 | Warning | FRONTEND_BASE_URL is empty, has no scheme, or points at localhost or a loopback address |
common.W004 | Warning | DEFAULT_FROM_EMAIL is on letsrevel.io but BASE_URL isn't, so you would be sending as our domain |
common.E002 | Error: check/migrate fail, the API doesn't start | CUSTOM_DOMAIN_URL_TEMPLATE has no {hostname}, has stray braces or other placeholders, or doesn't start with http:// or https://. Checked even with DEBUG on |
common.W005 | Warning | CUSTOM_DOMAIN_URL_TEMPLATE is not https:// (with DEBUG off) |
events.W001 | Warning | CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID are set but CUSTOM_DOMAIN_CNAME_TARGET is empty, so organizers aren't told which CNAME to create |
events.W002 | Warning | CUSTOM_DOMAIN_CNAME_TARGET is set but isn't a bare hostname (no scheme, path, port or spaces) |
The last four belong to the optional custom-domain feature (next section) and cost nothing when you don't use it: with the defaults none of them fires.
Custom domains (white-label)¶
Optional. Lets an organization serve its attendee pages from its own domain, such as events.example.org. A platform admin enables it per organization in the Django admin (custom_domain_enabled). The full setup, including Caddy and the frontend variables, is in the platform operator guide.
| Variable | Default | Purpose |
|---|---|---|
CLOUDFLARE_API_TOKEN | empty | Cloudflare API token with SSL and Certificates: Edit on the zone. Provisioning is on only when this and the zone id are set |
CLOUDFLARE_ZONE_ID | empty | The zone that owns the custom hostnames (Cloudflare for SaaS) |
CUSTOM_DOMAIN_CNAME_TARGET | empty | The fallback-origin hostname organizers CNAME to, e.g. customers.example.com. A bare hostname |
CUSTOM_DOMAIN_URL_TEMPLATE | https://{hostname} | Origin of an organization's active domain in email links. Must match the frontend's PUBLIC_CUSTOM_DOMAIN_URL_TEMPLATE |
PLATFORM_APEX_DOMAINS | empty | Comma-separated registrable domains reserved for the platform (organizations cannot claim them or anything under them), e.g. example.co.uk. Empty derives it from the last two labels of FRONTEND_BASE_URL/BASE_URL. Set this if your platform host sits under a multi-part public suffix such as co.uk |
On the frontend container, multi-host serving removes the pinned ORIGIN and sets HOST_HEADER=x-forwarded-host, PROTOCOL_HEADER=x-forwarded-proto, PLATFORM_HOSTS (tenant mode is off while it is unset), PLATFORM_ORIGIN and PUBLIC_CUSTOM_DOMAIN_URL_TEMPLATE (see the operator guide for the full list and defaults). With Cloudflare provisioning on, the web and Celery containers also need outbound HTTPS to api.cloudflare.com and cloudflare-dns.com, because a domain only activates once its ownership TXT record is visible in public DNS.
Without Cloudflare, leave the token and zone id empty. A domain request then only records the CNAME instruction, and a platform admin activates the domain in the Django admin once DNS and TLS are ready. TLS is yours to provide. Details in Running without Cloudflare. If you only ever serve one organization, you don't need any of this.
Feature flags¶
Most default to True (production behaviour); set them to False to drop the corresponding dependency. OIDC_PROVIDERS is the exception — it is opt-in and empty by default, since it needs credentials from an identity provider to do anything (the opt-in rule is explained in ADR-0008; the login flow itself in ADR-0016).
FEATURE_MALWARE_SCAN— whenFalse, uploads skip the ClamAV scan and are marked clean immediately. Lets you run without theantivirusprofile.FEATURE_TELEGRAM— whenFalse, Telegram is stripped from notification delivery and the linking endpoints return 404. Pair with leaving thetelegramprofile off.FEATURE_LLM_EVALUATION— whenFalse, automated questionnaire evaluation is disabled; manual evaluation still works. Lets you run without an OpenAI key.FEATURE_ORGANIZATION_CREATION— whenFalse, regular users cannot create organizations; staff and superusers still can. See below.OIDC_PROVIDERS— opt-in, empty by default. A comma-separated list of provider keys; for each key setOIDC_<KEY>_ISSUER,OIDC_<KEY>_CLIENT_ID,OIDC_<KEY>_CLIENT_SECRETand optionallyOIDC_<KEY>_NAME(button label),OIDC_<KEY>_SCOPES(defaultopenid email profile) andOIDC_<KEY>_TOKEN_AUTH(client_secret_post, the default, orclient_secret_basicwhen the client is registered for HTTP Basic authentication at the token endpoint). Registerhttps://<your-api-host>/api/auth/oidc/<key>/callbackas the redirect URI at the provider. Any OpenID Connect issuer works — Google (https://accounts.google.com), Keycloak (https://kc.example.org/realms/<realm>), Authentik, Zitadel. With the default scopes Google needs no app verification and does not apply its 100-user testing allowlist; publish the OAuth consent screen to "In production" to drop the "app in testing" warning. CustomOIDC_<KEY>_SCOPESthat include sensitive or restricted Google scopes require Google's verification regardless of publishing status. Sign-in (both linking to an existing account and creating a new one) trusts theemail_verifiedclaim from every configured issuer, so only list identity providers whose email verification you trust.OIDC_LOGIN_TOKEN_LIFETIME_SECONDS(default60) controls how long the one-time hand-off token from the callback to the frontend stays valid.OIDC_SIGNING_KEY_PATH+OAUTH_ISSUER— opt-in, unset by default; not aFEATURE_*flag but credential presence. With a readable RSA signing key and an issuer, Revel becomes an OAuth 2.1 / OpenID Connect provider (third-party apps, MCP hosts, "Sign in with" your instance), and/api/versionreportsfeatures.oauth_provider: true. Without them every provider route is a404. Setup, thechmod 644gotcha and tuning knobs: OAuth provider setup.FEATURE_OBSERVABILITY— master toggle for the metrics/traces/logs exporters and the async log queue. SetFalse(and drop theobservabilityprofile) on Slim. Legacy alias:ENABLE_OBSERVABILITY(deprecated). See Observability.
External listing platforms (Eventbrite)¶
| Variable | Purpose |
|---|---|
INTEGRATIONS_EVENTBRITE_CLIENT_ID | Eventbrite "API Key" from https://www.eventbrite.com/account-settings/apps |
INTEGRATIONS_EVENTBRITE_CLIENT_SECRET | Eventbrite "Client secret" |
INTEGRATIONS_RATE_RESERVE (default 200) | Calls kept in reserve in the shared per-app-key hourly budget so organizer actions (push, pause) never starve behind the reconcile sweep |
INTEGRATIONS_WEBHOOK_DELIVERY_RETENTION_DAYS (default 30) | Days a WebhookDelivery audit row or a finished ImportJob is kept before the daily prune sweep deletes it |
Both empty → the provider is not offered. Register the app's OAuth redirect URI as {BASE_URL}/api/integrations/eventbrite/callback and set the application URL to {BASE_URL}. Eventbrite rejects localhost / 127.0.0.1 redirect URIs at its CDN, so local testing needs a hosts-file alias (e.g. api.revel.test) and BASE_URL pointing at it. Rate limit: 2000 requests/hour per organization token and per app key (shared by every organization on this instance).
Syncing an event mirrors its name, summary, description, dates, venue, and public fixed-price tiers (online or free) as ticket classes on the connected platform; pay-what-you-can, members-only, seated, and offline/at-the-door tiers are skipped (they stay Revel-only), and a missing cover image is flagged but doesn't block the sync. The first push always creates a draft on the platform — making it live is a separate, explicit "publish" action. After that, auto-sync is opt-in: enable it per connection (every pushed event) or override it per event, and it only ever keeps already-pushed listings up to date — it never pushes a new event on its own.
Sold counts refresh from platform webhooks and a 15-minute reconcile; the reconcile stops when fewer than the reserve remain in the shared hourly budget.
Email¶
EMAIL_DRY_RUN— whenTrue, outbound email is logged instead of sent. Useful for test instances; never use it for a real instance, since verification mails won't be delivered.- SMTP settings — host, port, credentials, and from-address for real email.
ORG_EMAIL_DOMAIN(optional) — a dedicated sending domain such asmail.<your-domain>for mail organizations send to people: announcements, invitations, event updates and reminders. They go out as"<Org> via Revel" <org-slug@ORG_EMAIL_DOMAIN>. Unset, that mail uses the domain ofDEFAULT_FROM_EMAIL. A separate domain keeps the reputation of organization bulk mail apart from account mail (verification, password reset, tickets). Authenticate it at your SMTP provider first (see DNS → Sending domain): setting it before DKIM and DMARC pass makes all of that mail fail DMARC.EMAIL_WEBHOOK_SECRET(optional) — turns on the bounce and complaint webhook (below). Unset, the endpoint answers 404 and nothing is suppressed automatically.PENDING_INVITATION_DAILY_CAP(default200) — how many invitations each organization can send per UTC day to addresses that have no Revel account yet.0means unlimited. See pending invitation cap.ORG_NUDGE_REPLY_TO(optional) — Reply-To on the org setup nudges, so owners of stalled organizations can answer a person. Unset, the nudges have no Reply-To and the personal check-in nudge never goes out.ORG_NUDGE_SIGNATURE(defaultThe Revel team) signs that check-in. The daily nudge task ships disabled; enable "Send org setup nudges" under Periodic tasks in the Django admin after a dry run ofpython manage.py org_nudges.UNSUBSCRIBE_TOKEN_LIFETIME_DAYS(default3650) — how long unsubscribe links in emails stay valid. Mailbox providers show the one-click unsubscribe button on old mail too, so keep it long.
Which mail users can and can't opt out of, and what each unsubscribe does, is described in Email Sending & Opt-Out Policy.
Bounce and complaint webhook¶
Revel keeps a suppression list of addresses it must not email. Your email provider feeds it through a webhook; without one, bounces and spam complaints are not recorded and Revel keeps mailing those addresses. The receiver is built for Brevo transactional webhooks.
- Set
EMAIL_WEBHOOK_SECRETto a long random value (openssl rand -hex 32) and restart the stack. - In Brevo, add a transactional webhook with the URL
https://revel:<secret>@<API_DOMAIN>/api/email-events/brevo. Only the password is checked, so the username can be anything. If you create the webhook through Brevo's API instead, you can send the secret as a bearer token (Authorization: Bearer <secret>). - Tick the events Hard bounce, Spam (complaint), Blocked and Invalid email. Other events are accepted and ignored.
Wrong or missing credentials get a 401 and change nothing. To let an address receive mail again, delete its row under Email Suppressions in the admin. Rows survive account deletion, so a re-registered address keeps its bounce, complaint or opt-out.
Gmail complaints never reach the webhook
Gmail doesn't forward spam reports to email providers. Register your sending domain in Google Postmaster Tools and watch its spam rate there.
Single-org instances¶
For a community that only ever needs one organization, set:
With this flag off, normal users who hit "create organization" receive a 403, while your staff or superuser account can still create the single org from the admin or API. This is the recommended configuration for a personal or single-community instance — it keeps the public sign-up flow open while preventing arbitrary users from spinning up their own orgs on your box.