The settings every site needs
A site starts with no settings at all, but it is not usable: nobody can log in, no email is sent and emailed links cannot be built. This page lists the minimum for a real site and gives a template to start from. It is for whoever installs the server.
The minimum
| Setting | What it is for | Default |
|---|---|---|
SITE_URL | The public address of the site, such as https://choir.example.org. Every link the site puts in an email is built from it: login links for members and admins, links to tickets, unsubscribe links. So is the address each public page gives as its own to search engines and link previews. | none |
ADMIN_USERNAME, ADMIN_PASSWORD | The login for the admin panel. | none |
LICENSE_KEY | Your licence key. With it, the admin panel can tell you when a new version is out, and install it where that is possible. | none |
EMAIL_PROVIDER | How email is sent. The default, log, sends nothing: it writes each email to the server log. | log |
EMAIL_FROM | The name and address email comes from, such as Harmony Community Choir <noreply@example.org>. | none |
| The provider's own keys | For example RESEND_API_KEY, or SMTP_HOST and its companions. | none |
Without a database or storage setting the site keeps everything in a data folder beside the code: a SQLite database and the uploaded files. That is a sound choice for one server. The alternatives start at Choose a database and How files are stored.
SITE_URL
Write the address people type, with https:// and without a path. A slash at the end is removed.
In production the site never works the address out from the request, because whoever sends a request could then decide where an emailed link leads. So without SITE_URL, anything that needs to email a link fails. The site still starts, and the configuration check says:
SITE_URL is not set: emailed login links need the site address
From version 1.6.12 every public page is sent with its own proper address in it (<link rel="canonical"> and og:url), for search engines and for the preview shown when a link is shared. That address is SITE_URL's scheme and host followed by the page's path, so a path written in SITE_URL is not used for it. For the same reason as with emailed links, it is never taken from the request: behind a proxy the address a request arrives on is not the site's, and its Host header is whatever the caller wrote. Without SITE_URL pages go out with no address of their own, and a background photo uploaded to the site cannot be offered as the preview picture. If SITE_URL is the wrong address (http:// where the site is https://, or an old domain), search engines are told the wrong one. See How your site looks when a link is shared.
The host name in SITE_URL, and nothing else of the address, is also what the site sends with your licence key when it checks for updates. See Your licence key.
ADMIN_USERNAME and ADMIN_PASSWORD
One login shared by everyone who runs the site, unless you change how admins sign in. Until both are set nobody can log in.
Use a long random password. In production the configuration check warns about one shorter than 12 characters; the site still accepts it. Capitals in the username do not matter when logging in. They do in the password.
LICENSE_KEY
Every self-hosted site has a key, including the free Community edition. Without one the site runs, but never contacts the release server and cannot tell you about new versions. Get a licence key and the download explains where it comes from.
Email
Members log in with a link sent by email, so a site without working email has no member portal. Choose a provider and set EMAIL_PROVIDER, EMAIL_FROM and that provider's keys: Email: what the site sends and how to choose a provider.
Settings you may not need
| Setting | What it does | Default |
|---|---|---|
NODE_ENV | production or development. See below. | production |
PORT | The port the Node server listens on. Not used on Cloudflare. | 3001 |
ALLOWED_ORIGINS | Other origins whose pages may call this site's API with a visitor's login. | none |
LOG_LEVEL | Accepted, and does nothing. | info |
NODE_ENV
Leave it unset, or set it to production, on a real site. Any other value counts as not production, and relaxes these things for working on a laptop:
- Session cookies are not marked
Secure, so logging in works over plainhttp://. - Emailed links follow the address of the page that asked for them when
SITE_URLis not set. - Pages served from
http://localhostorhttp://127.0.0.1, on any port, may call the API. - The configuration check no longer reports a missing
SITE_URL, a short password,EMAIL_PROVIDER=logorSESSION_STORE=memory.
In production the cookies are Secure, so the browser only sends them back over HTTPS. A production site reached over plain HTTP lets nobody stay logged in. See Put it behind HTTPS.
PORT
Set it in the environment itself, not through a file or a secrets manager: the launcher reads it too. In Docker, leave it at 3001 and publish whichever host port you want.
ALLOWED_ORIGINS
The site and its API normally share one address, and this stays empty. It is only for a frontend served from a different origin, which the download does not do by itself. Give a comma-separated list of complete origins:
ALLOWED_ORIGINS=https://www.example.org,https://choir.example.org
Each one is matched exactly: scheme, host and port, with no path and no slash at the end. There are no wildcards.
LOG_LEVEL
The server reads LOG_LEVEL and uses it for nothing. Version 1.6.12 has one level of logging, and no setting changes how much is written.
A template to copy
The download has no .env file. Save this as .env beside package.json, fill in the first two blocks, and remove the # from any other line you need. Then make sure the file actually reaches the server.
# Choir Master CMS settings (Node and Docker).
# On Cloudflare the same names go in wrangler.toml [vars] and `wrangler secret put`.
# ---- Required ---------------------------------------------------------------
# The public address of the site. Every emailed link is built from it.
SITE_URL=https://choir.example.org
# The admin panel login. Use a long random password (12 characters or more).
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-to-something-long-and-random
# Your licence key.
LICENSE_KEY=
# ---- Email ------------------------------------------------------------------
# none | log | ses | smtp | resend | sendgrid | mailgun | postmark
# log sends nothing: emails are written to the server log.
EMAIL_PROVIDER=log
EMAIL_FROM="Harmony Community Choir <noreply@example.org>"
# EMAIL_REPLY_TO=board@example.org
# Where contact-form messages go. Defaults to the public contact email in Site Settings.
# CONTACT_FORM_TO=board@example.org
# -- smtp (Node only)
# SMTP_HOST=smtp.example.org
# SMTP_PORT=587
# SMTP_SECURE=false
# SMTP_USER=
# SMTP_PASS=
# -- ses
# AWS_REGION=us-east-1
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# EMAIL_BOUNCE_ADDRESS=bounce@example.org
# -- resend / sendgrid / mailgun / postmark
# RESEND_API_KEY=
# SENDGRID_API_KEY=
# MAILGUN_API_KEY=
# MAILGUN_DOMAIN=mg.example.org
# MAILGUN_REGION=us
# POSTMARK_SERVER_TOKEN=
# Any email setting can be given again for one kind of email, with
# TRANSACTIONAL_ or ANNOUNCEMENT_ in front.
# ANNOUNCEMENT_EMAIL_PROVIDER=smtp
# ANNOUNCEMENT_EMAIL_FROM="Harmony Community Choir <news@example.org>"
# EMAIL_BATCH_SIZE=15
# EMAIL_PER_SECOND=8
# ---- Admin sign-in ----------------------------------------------------------
# env: the one username and password above.
# table: each admin has an account and logs in by emailed link.
# ADMIN_AUTH=env
# ---- Database ---------------------------------------------------------------
# sqlite | postgres | mysql
# DB_PROVIDER=sqlite
# SQLITE_PATH=./data/choir.sqlite
# DATABASE_URL=postgres://choir:password@db.example.org:5432/choir
# DATABASE_SSL=false
# DB_AUTO_MIGRATE=true
# ---- File storage -----------------------------------------------------------
# local | s3
# STORAGE_PROVIDER=local
# STORAGE_LOCAL_DIR=./data/storage
# S3_ENDPOINT=
# S3_REGION=auto
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_PUBLIC_BUCKET=choir-public
# S3_PRIVATE_BUCKET=choir-private
# S3_FORCE_PATH_STYLE=true
# Leave unset and the site serves public files itself at /files/<key>.
# PUBLIC_FILES_URL=https://files.example.org
# UPLOAD_MAX_IMAGE_MB=10
# UPLOAD_MAX_GALLERY_MB=100
# UPLOAD_MAX_FILE_MB=50
# UPLOAD_MAX_TRACK_MB=50
# ---- Sessions ---------------------------------------------------------------
# database | redis | memory
# SESSION_STORE=database
# REDIS_URL=redis://redis:6379
# ADMIN_SESSION_HOURS=24
# MEMBER_SESSION_DAYS=30
# ---- Bot check on public forms ----------------------------------------------
# none | turnstile | hcaptcha | recaptcha
# CAPTCHA_PROVIDER=none
# CAPTCHA_SITE_KEY=
# CAPTCHA_SECRET_KEY=
# ---- Updates ----------------------------------------------------------------
# self | notify | off
# UPDATE_MODE=self
# stable | beta | dev
# UPDATE_CHANNEL=stable
# DATA_DIR=./data
# ---- Writing assistant in the guided setup (optional) -----------------------
# ANTHROPIC_API_KEY=
# AI_DAILY_LIMIT=30
# ---- Secrets ----------------------------------------------------------------
# env | vault | aws-secrets-manager | doppler | infisical
# SECRETS_PROVIDER=env
# Any setting can be read from a file instead:
# ADMIN_PASSWORD_FILE=/run/secrets/admin_password
# ---- Server -----------------------------------------------------------------
# PORT=3001
# ALLOWED_ORIGINS=
# ---- Read by docker-compose.yml only, not by the site ------------------------
# HTTP_PORT=8080
# POSTGRES_USER=choir
# POSTGRES_PASSWORD=change-me
# POSTGRES_DB=choir
# MINIO_ROOT_USER=choir
# MINIO_ROOT_PASSWORD=change-me-too
# DOMAIN=choir.example.org
This file holds the admin password and your email provider's key. On a server, make it readable only by the account the site runs as (chmod 600 .env), and never commit it to a repository.
If you pass the file to docker run --env-file, remove the quotation marks around EMAIL_FROM: that command keeps them as part of the value.
Afterwards
- Run the configuration check and clear every problem it lists.
- Start the site and log in at
/admin/login.
Every variable, including the ones this page leaves out, is in Environment variables.