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 ordering website, at build time | No. Every value is public. |
admin-dashboard/.env | The staff 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.
Where the settings live
One file, .env in the website's folder, read when the website 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.
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: the 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.
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 pass it with --env-file .env. 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, 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. |
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. |
CORS_ORIGIN | The website's and the dashboard's addresses, comma separated. A hosted payment's return address must be on one of them. Unset, only http://localhost:3030 and http://localhost:3031 are allowed; required in production. |
FRONTEND_URL | The dashboard's address, which its live notifications connect from. Required in production for those notifications. |
STOREFRONT_URL | The website's address, where a payment and a password-reset link send the customer. Defaults to the first CORS_ORIGIN entry. |
API_PUBLIC_URL | This API's public address, where Stripe and PayPal send the browser back first. Defaults to http://localhost on PORT. |
TRUST_PROXY | Optional. The number of reverse proxies in front of the API, so the per-address limits count the real visitor. |
RATE_LIMIT_LOGIN | Optional. Failed sign-ins one address may make on a sign-in form in 15 minutes before 429, 10 by default. |
RATE_LIMIT_ORDERS, RATE_LIMIT_PAYMENT_SESSION, RATE_LIMIT_TRACKING, RATE_LIMIT_REGISTER, RATE_LIMIT_CONTACT, RATE_LIMIT_PASSWORD_RESET | Optional. The per-address limits on the public forms; .env.example gives each default and its window. |
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.
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=your-mysql-host
DB_PORT=3306
DB_USERNAME=your-mysql-username
DB_PASSWORD=your-mysql-password
DB_DATABASE=your-database-nameThen run yarn seed for the demo restaurant, 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).
Ordering website and staff 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_MEDIA_HOSTNAME | Both | The https host of your picture bucket, such as pub-1234.r2.dev, or of the API when it serves the pictures on a public domain. Pictures on it are resized by Next.js; any other picture is shown as it is. Host only. Leave it empty while the API runs on localhost. |
NEXT_PUBLIC_WEBSOCKET_BASE_URL | Dashboard | The API's address without /api, for live orders and notifications. |
NEXT_PUBLIC_DASHBOARD_URL | Dashboard | The dashboard's own address, for absolute links and the installable app. |
NEXT_PUBLIC_STOREFRONT_URL | Dashboard | Where the "View storefront" link in the navbar goes. |
NEXT_PUBLIC_MAP_STYLE_LIGHT, NEXT_PUBLIC_MAP_STYLE_DARK | Website (light only), dashboard | Optional map tile styles. Empty uses OpenFreeMap's public styles, which need no key. |
API_INTERNAL_URL | Website | Optional, server only, read when the container starts. Where the website's own server reaches the API when the browser's address does not work from inside it, as in Docker Compose (http://api:8000/api). 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.
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 | Your bucket's public host, with the R2_* variables in back-end/.env. |
SEED_DEMO_DATA | true | false starts with empty tables and no accounts instead of the demo restaurant. 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 | USD | The currency sent with every tracked value. |
Change a port when another program already uses it, such as ADMIN_PORT=3041. The API's CORS_ORIGIN, FRONTEND_URL, STOREFRONT_URL, API_PUBLIC_URL and PUBLIC_MEDIA_URL, and the frontends' API address, 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 payments, mail, media and AI 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 food-studio-data volume.
Media storage
The API serves the seeded pictures itself, from public/media/food-studio, at /media/food-studio/…. Uploads from the dashboard (dish photos, profile pictures, the AI studio's results) 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. The seed writes it into every picture's address, so set it before the first seed on a server. 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, and give both frontends the bucket's public host through NEXT_PUBLIC_MEDIA_HOSTNAME (with Docker Compose, MEDIA_HOSTNAME). Set all five or none.
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.devPayments
Checkout sends the customer to Stripe's or PayPal's own page. Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET, or PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET and PAYPAL_WEBHOOK_ID (with PAYPAL_ENV, sandbox by default). With no provider set, checkout offers a built-in demo payment that marks an order paid without moving money.
STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secretPassword-reset links are sent through Resend. Both values are needed: a sending-access key, and a From address on a domain verified in Resend. Without them "Forgot password" still answers as usual, no message is sent, and the log says mail is not configured.
RESEND_API_KEY=re_your-sending-access-key
MAIL_FROM=Food Studio <noreply@your-domain.com>
MAIL_MAX_PER_ADDRESS_PER_DAY=5The link opens the website's reset page at STOREFRONT_URL, in the customer's language, and works once within an hour. One address receives at most MAIL_MAX_PER_ADDRESS_PER_DAY messages a day, 5 by default.
AI features
Included with your purchase. Sign in to read, or open it in your download.
Turning on the AI features: the provider keys, the models each feature uses, and the background-removal 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.
Running a public demo: the demo switches, per-visitor accounts and what visitors can change.
Going live
Before you deploy anywhere public:
- In the API's
.env, setNODE_ENV=productionand a long randomJWT_SECRETof your own. - Set
CORS_ORIGINto the website's and the dashboard's addresses,FRONTEND_URLto the dashboard's,STOREFRONT_URLto the website's,API_PUBLIC_URLto the API's, andPUBLIC_MEDIA_URLto the API's address followed by/media. - On a new database, run
yarn build, thenyarn seed:prodonce (oryarn db:sync:prodfor empty tables), thenyarn start:prod. There are no migrations: the seeder is the schema tool. - Keep the uploads on persistent storage: a bucket, or
MEDIA_UPLOAD_DIRon a disk that survives a deploy. - Point your host's health check at
/api/health, and setTRUST_PROXYwhen a reverse proxy stands in front of the API. - Build each frontend with
NEXT_PUBLIC_API_BASE_URLat your deployed API; the dashboard also withNEXT_PUBLIC_WEBSOCKET_BASE_URL,NEXT_PUBLIC_DASHBOARD_URLandNEXT_PUBLIC_STOREFRONT_URL. - On the website, set
domain.urlinsrc/config/brand.config.ts, and rewrite the privacy policy and terms inmessages/legalfor your business. - In the dashboard, set your kitchens, their hours and delivery postcodes, your fees and promo codes, and switch the service clock to Real: the seed starts it on the demo clock, which keeps every kitchen open.
- Change the seeded passwords, or delete the accounts, and switch payments to live keys.
Not for a restaurant with real orders
yarn railway:setup builds, fetches the background-removal model, drops every table and seeds, every time it runs, and yarn drop:prod drops every table. They suit a new environment, never the build command of a live restaurant.