Environment variables
What each setting in the API's and the frontends' .env files does, and which ones you need.
For the Full Stack package
Where the settings live
| File | Read by | Holds secrets |
|---|---|---|
back-end/.env | The API, with yarn dev and inside Docker Compose | Yes. Never commit it. |
storefront/.env | The website: the NEXT_PUBLIC_* values at build time, REVALIDATE_SECRET and API_INTERNAL_URL on its server | Only REVALIDATE_SECRET, which never reaches the browser |
admin-dashboard/.env | The organiser dashboard, at build time | No. Every value is public. |
.env beside docker-compose.yml | Docker Compose, for the full stack, and the API container, which reads it before back-end/.env | REVALIDATE_SECRET on a server open to the internet, and OWNER_PASSWORD when you put it there |
Create each app's file from the .env.example beside it, which documents every variable: cp .env.example .env. The examples work as they are for a local run, and the Docker Compose start needs no file at all.
Where the settings live
One file, .env in the website's folder. The NEXT_PUBLIC_* values in it are read when the website is built and are public, so they never hold a secret. REVALIDATE_SECRET and API_INTERNAL_URL are read by the website's own server at run time and never reach the browser. Create it from .env.example, which points at an API on http://localhost:8000: cp .env.example .env. With Docker, pass the NEXT_PUBLIC_* values as --build-arg and the other two with -e.
Where the settings live
One file, .env in the dashboard's folder, read when the dashboard is built. Every value in it is public, so it never holds a secret: provider keys live in the API's .env. Create it from .env.example, which points at an API on http://localhost:8000: cp .env.example .env. With Docker, pass the same values as --build-arg.
Where the settings live
One file, .env in the API's folder, read by the API with yarn dev, and by a container when you add --env-file .env -e NODE_ENV=production to its docker run. It holds secrets: never commit it. Create it from .env.example, which documents every variable and works as it is for a local run: cp .env.example .env.
The Docker image sets its own defaults, so docker run works without a file: SQLite at /data/database.sqlite, uploads in /data/uploads, CSV exports in /data/exports, CORS_ORIGIN for the two local frontends, FRONTEND_URL=http://localhost:3031, STOREFRONT_URL=http://localhost:3030, PUBLIC_API_URL=http://localhost:8000, PUBLIC_MEDIA_URL=http://localhost:8000/media and SEED_DEMO_DATA=false. Override any of them with -e.
API essentials
The API checks JWT_SECRET and, in production, CORS_ORIGIN before it starts. If one is missing or unusable, it stops with one line saying what to fix.
| Variable | What it does |
|---|---|
NODE_ENV | development locally, which creates and updates the tables on start. production on a live server, which never touches the tables. |
PORT | The API's port, 8000. Both frontends point at it. |
DB_TYPE | sqlite (the default when unset) or mysql. |
SQLITE_DATABASE | Path of the SQLite file, ./database.sqlite in .env.example. |
JWT_SECRET | Signs every sign-in, and the ticket QR codes while TICKET_SECRET is unset. Required. The example value is accepted outside production only. |
JWT_EXPIRATION | How long a sign-in lasts, 7d when unset. |
CORS_ORIGIN | The website's and the dashboard's addresses, comma separated. Unset, only http://localhost:3030 and http://localhost:3031 are allowed, never every origin; required in production. |
FRONTEND_URL | The dashboard's address, the one origin its live connections (notifications and permission changes) accept, http://localhost:3031 when unset. |
STOREFRONT_URL | The website's address: payment pages send buyers back to it, ticket emails link to it, and the API refreshes the website's pages through it. |
PUBLIC_API_URL | The API's own public address, used in the signed links for ticket PDFs, calendar files and CSV exports, and in PayPal's return address. |
REVALIDATE_SECRET | Optional. The same value as REVALIDATE_SECRET in the website's .env. With it, the API tells the website to rebuild an event, venue or organiser page as soon as it is published, edited, cancelled or sells out; without it those pages refresh on their own within an hour. |
STOREFRONT_INTERNAL_URL | Optional. Where the API reaches the website when that differs from STOREFRONT_URL, as inside Docker (http://storefront:3030). |
TICKET_SECRET | Optional. Signs every ticket's QR code and the download links; unset, JWT_SECRET is used. Set it once, before you sell: changing it later voids every QR code already issued. |
EXPORT_DIR | Optional. Where CSV exports are written before they are downloaded, ./exports when unset. |
TRUST_PROXY | Optional. The number of reverse proxies in front of the API, so the sign-in, contact form and door scan limits count the real visitor. |
RATE_LIMIT_LOGIN | Optional. Failed sign-ins one address may make in 15 minutes before 429, 10 by default. |
SEED_DEMO_DATA | Read only by the Docker image, on a volume with no database yet: true creates it with the four sample organisers and their accounts, false with the tables only, after which the API creates your own workspace from the owner variables below. |
Generate your own JWT_SECRET with:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"Every optional feature below is off while its lines in .env.example stay commented out.
Your own workspace and Owner
Read only on a database with no staff account and no workspace (the tables from yarn db:sync, or a Docker volume started with SEED_DEMO_DATA=false). The API's first start then creates the roles, the site settings, the event categories, one workspace and its Owner from them, once. As soon as any staff account exists they are ignored.
OWNER_EMAIL=you@your-domain.com
OWNER_PASSWORD=a-long-password-of-your-own
OWNER_NAME=Your Name
WORKSPACE_NAME=Your organisation| Variable | What it does |
|---|---|
OWNER_EMAIL | The Owner's sign-in email for the dashboard. |
OWNER_PASSWORD | At least 10 characters. With NODE_ENV=production the example from .env.example and the sample accounts' password are refused. Never printed, and safe to remove once the Owner exists. |
OWNER_NAME | Optional, Owner when unset. Changed later on the Owner's profile. |
WORKSPACE_NAME | Optional, Ticketory when unset. Your organisation's name, changed later under Settings, General. |
In the Full Stack's Docker Compose they can go in the .env beside docker-compose.yml, with SEED_DEMO_DATA=false: Compose passes that file to the API first and then back-end/.env, whose value wins where both set one. Without the variables, the API starts and says which ones to set.
Payments
Checkout sends the buyer to Stripe's or PayPal's own page, so card details are never typed on your site. Each provider is offered once its keys are set. Use test keys first: they move no money.
STRIPE_SECRET_KEY=sk_test_your_key
STRIPE_WEBHOOK_SECRET=whsec_your_secret
PAYPAL_CLIENT_ID=your-client-id
PAYPAL_CLIENT_SECRET=your-client-secret
PAYPAL_WEBHOOK_ID=your-webhook-id
PAYPAL_ENV=sandbox| Variable | What it does |
|---|---|
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | Stripe's hosted Checkout. A key starting sk_test_ or rk_test_ is test mode. The webhook is https://<your-api>/api/webhooks/stripe, sending checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed and refund.updated. |
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID | PayPal. The webhook is https://<your-api>/api/webhooks/paypal (PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.DENIED, PAYMENT.CAPTURE.REFUNDED), and its id goes in PAYPAL_WEBHOOK_ID. |
PAYPAL_ENV | sandbox or live. Unset means sandbox. |
PAYMENTS_SIMULATOR | The test payment, which pays an order without moving money. auto (the default) offers it while no live key is set, so test keys keep it; on or off overrides that. |
Free tickets need none of this: an order with nothing to pay is confirmed at checkout, with or without keys. Ticketory charges no tax by itself, so set up tax for your country before taking real payments.
Tickets, sign-up codes, password resets, refund news and event reminders are sent through Resend once both variables are set. Until then every email is written to the API's log instead, which is where a sign-up code is read on a fresh install (with Docker: docker compose logs api).
RESEND_API_KEY=re_your_key
MAIL_FROM=Ticketory <tickets@your-domain.com>
EVENT_REMINDER_HOURS=24EVENT_REMINDER_HOURS is how long before an event its reminder goes out: 24 when unset, at most 168 (a week), and 0 turns reminders off. Every email about an order (tickets, refunds, the cancellation notice and the reminder) is written in the language the order was placed in.
Use MySQL instead of SQLite
Create an empty database, then set the driver and the connection in the API's .env:
DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=
DB_DATABASE=ticketoryThen run yarn seed for the sample data, or yarn db:sync for the tables with no data. The running API only creates and updates the tables itself when NODE_ENV=development, so with NODE_ENV=production one of those commands runs before the first start (yarn seed:prod or yarn db:sync:prod after yarn build). The Docker image does this by itself on SQLite only: on MySQL, run it once yourself.
Pictures and uploads
The API serves the seeded pictures itself, from public/media/ticketory (the category symbols, the brand marks, the assistant's emblem and the studio's sample photos), at /media/ticketory/…. Uploads from the dashboard (the empty venue photos for the studio, the photos attached to an assistant question) and the studio's finished concepts are stored on the API's disk under MEDIA_UPLOAD_DIR and served at /media/uploads/…, unless a bucket is set.
PUBLIC_MEDIA_URL=http://localhost:8000/media
MEDIA_UPLOAD_DIR=./public/media/uploadsPUBLIC_MEDIA_URL is the address /media is reached at from a browser. Every picture address is built from it, and the API moves the stored ones onto it when it starts, so moving the API keeps every picture. On a server, point MEDIA_UPLOAD_DIR at persistent storage; with Docker it is in the data volume.
To send uploads to Cloudflare R2, or any S3-compatible bucket, set all five variables. Give both frontends the bucket's public host through NEXT_PUBLIC_MEDIA_HOSTNAME (with Docker Compose, MEDIA_HOSTNAME) so Next.js resizes those pictures.
R2_ACCESS_KEY_ID=your-r2-access-key-id
R2_SECRET_ACCESS_KEY=your-r2-secret-access-key
R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
R2_BUCKET_NAME=your-bucket-name
R2_PUBLIC_URL=https://your-public-url.r2.devPictures are re-encoded as JPEG on upload, at most 1920 pixels wide. Videos are accepted as MP4, WebM, MOV or M4V, up to 150 MB.
Website and organiser dashboard
Every NEXT_PUBLIC_* value is compiled into the JavaScript the browser loads, and anyone who opens the page can read it. Never put a secret in one, and rebuild (or restart yarn dev) after changing one.
| Variable | App | What it does |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | Both | The API's address including /api, http://localhost:8000/api by default. |
NEXT_PUBLIC_SITE_URL | Both | The website's public address, http://localhost:3030 by default. On the website, canonical URLs, language alternates, sharing cards, robots.txt and the sitemap are built from it; in the dashboard, the "View website" link and the event previews. |
NEXT_PUBLIC_DASHBOARD_URL | Both | The dashboard's address. On the website, where "Sell tickets" and the organiser links send an organiser to sign in (http://localhost:3031). In the dashboard, the base of the picture shown when a dashboard link is shared; empty uses domain.url in src/config/brand.config.ts. |
NEXT_PUBLIC_MEDIA_HOSTNAME | Both | Optional. The https hosts serving the pictures, comma separated: your bucket's public host, such as pub-1234.r2.dev, the API's domain, or both. Pictures on them are resized by Next.js; any other picture loads as it is. Hosts only, without https://. Leave it empty while the API runs on localhost. |
NEXT_PUBLIC_WEBSOCKET_BASE_URL | Dashboard | The API's address without /api, for live notifications. |
NEXT_PUBLIC_MAP_STYLE_LIGHT, NEXT_PUBLIC_MAP_STYLE_DARK | Dashboard | Optional map tile styles. Unset, the light theme draws OpenFreeMap's Positron style and the dark theme its Dark style, which need no key. |
NEXT_PUBLIC_PLACE_SEARCH_URL | Dashboard | The place search in the venue editor's map picker, https://photon.komoot.io/api when unset. Any geocoder that answers the same shape works. |
REVALIDATE_SECRET | Website | Server only, read at run time. The secret the API sends to the website's POST /api/revalidate after an event changes; the same value as in the API's .env. Empty, that route refuses every call and pages refresh within an hour. |
API_INTERNAL_URL | Website | Optional, server only, read at run time. Where the website's own server reaches the API (event page titles, the first render of the event and venue pages, the sitemap) when the browser's address does not work from inside it, as in Docker (http://api:8000/api in Docker Compose). Leave it unset elsewhere. |
BUILD_STANDALONE | Both | true makes yarn build emit a self-contained server, which the Dockerfiles set. Leave it unset otherwise. |
The website's marketing tags are NEXT_PUBLIC_* values too: Google Tag Manager (NEXT_PUBLIC_GTM_ID), GA4 (NEXT_PUBLIC_GA4_MEASUREMENT_ID), Meta, TikTok, Snapchat and Pinterest, each switched on by one id; a blank id loads no script. NEXT_PUBLIC_ANALYTICS_CURRENCY (USD by default) is the currency sent with ticket prices, and NEXT_PUBLIC_ANALYTICS_DEBUG=true logs every tracked event to the browser console.
Docker Compose options
Nothing has to be set for a local run. To change something, put it in a .env file beside docker-compose.yml and run docker compose up --build again: the frontends compile these values in.
| Variable | Default | What it does |
|---|---|---|
SITE_PORT | 3030 | The website's port on your computer. |
ADMIN_PORT | 3031 | The dashboard's port on your computer. |
API_PORT | 8000 | The API's port on your computer. |
SITE_URL, ADMIN_URL, API_URL | The local addresses on those ports | Where the browser reaches each app. Set all three when serving the stack on your own domains. |
MEDIA_HOSTNAME | Empty | The https hosts serving the pictures, comma separated: your bucket's public host (with the R2_* variables in back-end/.env), the API's own domain, or both. |
REVALIDATE_SECRET | A local value | The secret the API and the website share, so a change made in the dashboard reaches the website's pages at once. Set your own long random value on a server open to the internet. |
SEED_DEMO_DATA | true | false starts with empty tables instead of the four sample organisers, and with the owner variables set the API then creates your own workspace and its Owner. It matters only on a new volume. |
OWNER_EMAIL, OWNER_PASSWORD, OWNER_NAME, WORKSPACE_NAME | Empty | Your workspace's Owner and name, read only on a database with no staff account: see Your own workspace and Owner above. |
NEXT_PUBLIC_GTM_ID, NEXT_PUBLIC_GA4_MEASUREMENT_ID, NEXT_PUBLIC_META_PIXEL_ID, NEXT_PUBLIC_TIKTOK_PIXEL_ID, NEXT_PUBLIC_SNAPCHAT_PIXEL_ID, NEXT_PUBLIC_PINTEREST_TAG_ID | Empty | The website's marketing tags, all optional. |
NEXT_PUBLIC_ANALYTICS_CURRENCY, NEXT_PUBLIC_ANALYTICS_DEBUG | USD, false | The currency sent with every tracked price, and logging every event to the browser console. |
NEXT_PUBLIC_MAP_STYLE_LIGHT, NEXT_PUBLIC_MAP_STYLE_DARK, NEXT_PUBLIC_PLACE_SEARCH_URL | Empty | The dashboard's map tiles and place search; empty uses OpenFreeMap and Photon. |
Change a port when another program already uses it, such as ADMIN_PORT=3041. The API's CORS_ORIGIN, FRONTEND_URL, STOREFRONT_URL, PUBLIC_API_URL and PUBLIC_MEDIA_URL, and the frontends' API, website and dashboard addresses, are all built from the ports and the three addresses, so they follow by themselves.
The API container reads this .env first and then back-end/.env when it exists, whose value wins where both set one, so the optional keys are set in one place for both yarn dev and Docker. The compose file wins for the values that differ inside a container: the database path, the upload and export folders, the port, NODE_ENV=production and the addresses above. The database, the uploads and the CSV exports live in the ticketory-data volume.
AI features
Included with your purchase. Sign in to read, or open it in your download.
Turning on the AI event assistant and the AI venue studio: the provider keys and the image model.
The MCP server
Included with your purchase. Sign in to read, or open it in your download.
Letting coding agents and other MCP clients use the assistant's tools: the key and the endpoint.
Demo mode
Included with your purchase. Sign in to read, or open it in your download.
The switch for a public demo, and why it stays off for a real organiser.