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 stays website, at build time | No. Every NEXT_PUBLIC_* value is public. |
admin-dashboard/.env | The booking dashboard, at build time | No. Every value is public. |
.env beside docker-compose.yml | Docker Compose, for the full stack | No |
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, read when the website is built. Every NEXT_PUBLIC_* value in it is public, so it never holds a secret. 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 dashboard's folder, read when the dashboard is built. Every value in it is public, so it never holds a secret: payment and AI 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, CORS_ORIGIN for the two local frontends, FRONTEND_URL=http://localhost:3031, SITE_URL=http://localhost:3030, 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 (as in .env.example) or mysql. Unset means MySQL, so keep the line. |
SQLITE_DATABASE | Path of the SQLite file, ./database.sqlite by default. |
JWT_SECRET | Signs every sign-in. Required. The example value is accepted outside production only. |
JWT_EXPIRATION | How long a sign-in lasts, 7d by default. The sign-in response reports it as expires_in. |
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, which its live notifications connect from, http://localhost:3031 when unset. |
SITE_URL | The website's address. A payment processor sends the guest back here, to their own booking page, so it has to be the address a guest browses. |
TRUST_PROXY | Optional. The number of reverse proxies in front of the API, so the per-address limits (sign-in, bookings, questions, reviews) 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 demo company, false with the tables only. |
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.
Payments
Without any of these the shipped simulator takes every card and PayPal booking, so the whole checkout works with no merchant account, and the API's start-up log says Payment simulator active while it does. It confirms those bookings without taking any money, so set Stripe, PayPal or both before taking real bookings: the simulator then switches off, the checkout offers only the configured methods plus pay on arrival, and a method with no processor is refused. The Payments tab of the dashboard's settings shows which processors are configured, whether their keys are test or live, and whether the simulator is in use, never the keys themselves.
STRIPE_SECRET_KEY=your-stripe-secret-key
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-signing-secret
PAYPAL_CLIENT_ID=your-paypal-client-id
PAYPAL_CLIENT_SECRET=your-paypal-client-secret
PAYPAL_WEBHOOK_ID=your-paypal-webhook-id
PAYPAL_ENV=sandbox| Variable | What it does |
|---|---|
STRIPE_SECRET_KEY | Turns on card payments through Stripe's hosted Checkout. A sk_test_ key moves no real money. |
STRIPE_WEBHOOK_SECRET | The signing secret of your Stripe webhook endpoint, or the one stripe listen prints. Without it every Stripe webhook is refused, so a card booking never settles. |
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET | Turn on PayPal through Orders v2, both together. |
PAYPAL_WEBHOOK_ID | The id of the PayPal webhook subscription, which verifies PayPal's signature. |
PAYPAL_ENV | sandbox (the default) or live. |
Pay on arrival needs no key: it is the allow_pay_on_arrival switch in the dashboard's Payments tab, on in the seed.
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=hollinaThen run yarn seed for the demo company, 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).
Photographs and uploads
The API serves the seeded photographs itself, from public/media/hollina, at /media/hollina/…. Uploads from the dashboard (stay photographs, destination covers, profile pictures) 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 photograph address is built from it, and the API moves the stored ones onto it when it starts, so moving the API keeps every photograph. 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 photographs.
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.devPhotographs are re-encoded on upload as JPEG, at most 1920 pixels wide, with a smaller copy for stay cards. Videos are accepted as MP4, WebM, MOV or M4V, up to 150 MB.
Stays website and booking 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 these files, and rebuild 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 sign-in page's wordmark, "Back to website" and privacy link, and the stay editor's "View stay". |
NEXT_PUBLIC_MEDIA_HOSTNAME | Both | Optional. The https hosts serving the photographs, comma separated: your bucket's public host, such as pub-1234.r2.dev, the API's domain, or both. Photographs on them are resized by Next.js; any other loads as it is. Hosts only, without https://. Both apps read the same list, so one value serves both. 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 | Website | Optional MapLibre styles for the stay page's map. Unset, both themes draw OpenFreeMap's Positron style, which needs no key. |
API_INTERNAL_URL | Website | Optional, server only, read at run time rather than at build time. Where the website's own server reaches the API 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, GA4, Meta, TikTok, Snapchat and Pinterest, each switched on by one id.
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 photographs, comma separated: your bucket's public host (with the R2_* variables in back-end/.env), the API's own domain, or both. |
SEED_DEMO_DATA | true | false starts with empty tables and no accounts instead of the demo company. It matters only on a new volume. |
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 | Empty | The stay page's map styles; empty uses OpenFreeMap's Positron. |
Change a port when another program already uses it, such as ADMIN_PORT=3041. The API's CORS_ORIGIN, FRONTEND_URL, SITE_URL and PUBLIC_MEDIA_URL, and the frontends' API and website addresses, are all built from the ports and the three addresses, so they follow by themselves.
The API container also reads back-end/.env when it exists, so the payment, AI, R2 and MCP 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 folder, the port, NODE_ENV=production and the addresses above. The database and the uploads live in the hollina-data volume.
The AI assistant
Included with your purchase. Sign in to read, or open it in your download.
Turning on the AI assistant: the Google Gemini key and what it is billed for.
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 company.