TrayPage Docs

Configuration reference

Every environment variable and binding for self-hosted TrayPage, and what each one means.

This is the list of settings for the self-hosted edition. On Docker / VPS you set them in .env (deploy/docker/.env.example); on Cloudflare you set them in vars in wrangler.jsonc and as secrets.

"Required" means TrayPage cannot start, or cannot work correctly, without it.

Basics

NameRequiredMeaning
APP_ORIGINRequired in productionThe app URL (for example https://traypage.example.com). Used to build share URLs and sign-in links, and as the allowed origin for authentication. Required for Docker
VIEWER_ORIGINRequired in productionThe URL of the viewer host that serves page content (for example https://view.traypage.example.com). Use a different host name from APP_ORIGIN. Required for Docker
VIEWER_HOSTOptionalThe host name (including port) of the viewer host. Set it on Cloudflare. On Docker, if unset, the host of VIEWER_ORIGIN is used
BETTER_AUTH_SECRETRequiredThe key that signs sign-in sessions. Generate one with something like openssl rand -hex 32. Changing it signs everyone out
VIEWER_TICKET_SECRETRequired on CloudflareThe key for the short-lived (5 minute) signed tickets handed to the viewer host. On Docker, BETTER_AUTH_SECRET is used if unset. On Cloudflare, use a different value from BETTER_AUTH_SECRET
TRAYPAGE_EDITIONOptionalself_hosted removes usage limits and trials, and disables operator-only features and scheduled jobs. The Docker setup sets it automatically and the Cloudflare template already sets it. If unset or unrecognized, behavior is the same as tray.page (hosted)
TRAYPAGE_LICENSE_KEYOptionalThe Enterprise license key (starts with tp1.). See License and Enterprise features
ENVIRONMENTOptionalproduction keeps the test-only login disabled at all times and ignores the setting that turns off the sign-in rate limit (AUTH_RATE_LIMIT). The Cloudflare template already sets production

Database

NameApplies toMeaning
DATABASE_URLDockerThe Postgres connection string (for example postgres://user:password@host:5432/db?sslmode=require). If unset on Docker, the bundled Postgres is used
DATABASE_POOL_MAXDockerMaximum connection pool size. Defaults to 10
POSTGRES_PASSWORDDocker (compose)Password of the bundled Postgres. Decide it before the first start
HYPERDRIVECloudflareThe Hyperdrive binding. You pick an existing configuration in the button flow

The app creates tables automatically on startup (on Cloudflare, on the first access).

Page content storage

NameApplies toMeaning
PAGE_BUCKETCloudflareThe R2 bucket binding. Created automatically by the button
S3_ENDPOINTDockerEndpoint of the S3-compatible storage. If unset on Docker, the bundled SeaweedFS is used
S3_BUCKETDockerBucket name. Defaults to traypage-pages
S3_ACCESS_KEY_IDDockerAccess key ID
S3_SECRET_ACCESS_KEYDockerSecret access key
S3_REGIONDockerRegion. The AWS region for AWS S3, or auto for R2
S3_ADDRESSING_STYLEDockerpath (default) or virtual

On Cloudflare, PAGE_BUCKET is used when present. On Docker, S3-compatible storage is used when the S3_* values are complete.

Email

NameApplies toMeaning
EMAIL_FROMBothThe sender (for example TrayPage <noreply@example.com>)
RESEND_API_KEYBothFor compatibility with older versions. Email is sent through Resend only when this is set, and it takes priority over the EMAIL binding and SMTP
SMTP_HOSTDockerSMTP server host name. Used when RESEND_API_KEY is not set (the default on Docker)
SMTP_PORTDockerPort. If unset, 465 when SMTP_SECURE is enabled, otherwise 587
SMTP_SECUREDockertrue uses TLS from the start (port 465); otherwise STARTTLS
SMTP_USER / SMTP_PASSWORDDockerSMTP credentials (omit if no authentication is needed)
EMAILCloudflareThe Cloudflare Email Service binding. Included in the template. When present, email is sent through it (the default on Cloudflare; the sender domain must be registered)
MAILPIT_URLDevelopmentWhen no sender is configured, email is sent to this Mailpit API (default http://localhost:8025). The bundled compose setup sends to Mailpit over SMTP by default

Sign-in

NameMeaning
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETThe OAuth client for Google sign-in. The redirect URI is <APP_ORIGIN>/api/auth/callback/google
NEXT_PUBLIC_GOOGLE_AUTHSet to 1 to show Google sign-in on the sign-in screen

Sign-in rate limit

Authentication actions, such as sign-in (sending and opening the email link, passwords) and connecting AI agents (OAuth), are always limited per client IP address. Over the limit, requests get "Too many requests" (HTTP 429) for a while, usually about a minute. Counts are stored in the database, so they add up across multiple servers. It is on without any configuration.

NameApplies toMeaning
TRUSTED_CLIENT_IP_HEADERDockerThe header to read the client IP address from. The bundled compose setup (behind Caddy) sets x-forwarded-for, and so does the AWS template. For headers that are appended to, such as X-Forwarded-For, the last value is used. If unset, the address connecting to the app is used. Not needed on Cloudflare (the client IP that Cloudflare adds is used)
  • If you put another load balancer or CDN in front of Caddy, everyone is counted as the same IP (the one in front) and hits the limit together. In that case, set trusted_proxies in the Caddyfile so Caddy keeps the client IP passed on by the proxy in front (see "認証のレート制限" (auth rate limit) in deploy/docker/README.md).
  • If you expose the app directly without a proxy, do not set TRUSTED_CLIENT_IP_HEADER. Clients can write any header they like, which would let them get around the limit.

Server (Docker)

NameMeaning
PORTThe port the app container listens on. Defaults to 8080. In compose it is not exposed; only Caddy connects to it
HOSTThe address to listen on. Defaults to 0.0.0.0

Compose settings (Docker)

NameMeaning
COMPOSE_PROFILESWhich services to start: postgres (bundled Postgres), s3 (bundled SeaweedFS), and mailpit (a local mail catcher), comma-separated
TRAYPAGE_IMAGEThe image to use. Defaults to ghcr.io/8d-inc/tray-page:latest. In production, pin a version such as :v1.2.3
HTTP_PORT / HTTPS_PORTPorts Caddy listens on. 80 and 443 in production; locally, the same port written in APP_ORIGIN
MAILPIT_UI_PORTPort of the Mailpit UI. Defaults to 8025 (exposed only on your own machine)

Cloudflare bindings

NameMeaning
HYPERDRIVEConnection to the database (required)
PAGE_BUCKETR2 bucket for page content (required)
EMAILEmail Service, for sending email
BROWSERBrowser Run, for faithful PDF export (optional)
REVIEW_SESSIONSDurable Objects, for real-time review notifications (optional)
ASSETSStatic files for the UI (set in the template; no change needed)

Settings you normally do not set

The following are for operating tray.page (hosted), development, or testing. Do not set them on a self-hosted instance.

NamePurpose
E2E_LOGIN_SECRETLogin for automated tests. Always disabled when ENVIRONMENT=production
AUTH_RATE_LIMIToff removes the sign-in rate limit (for development and automated tests). Ignored when ENVIRONMENT=production
OPERATOR_EMAILS, OPERATOR_SLACK_WEBHOOK_URL, DEFAULT_TRIAL_DAYSOperator features of the hosted service. Disabled in the self-hosted edition
SUPPORT_EMAIL, SUPPORT_EMAILSContact addresses for the hosted service's support form
STORAGE_DIRA local folder used when no storage is configured at all (for tests)
BETTER_AUTH_URLOn Docker this is set from APP_ORIGIN automatically
NEXT_PUBLIC_APP_URLAn old setting name. Use APP_ORIGIN instead