Environment variables
What each setting in the API's and the dashboard's .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. |
dashboard/.env | The 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 next to it, which documents every variable: cp .env.example .env. The examples work as they are for a local run, except the dashboard's two API addresses, which you fill in to leave the sample data.
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. Leave it out to run on the sample data; create it from .env.example when you connect an API: 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.
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. Variables already set in the environment win over the file.
| Variable | What it does |
|---|---|
NODE_ENV | development locally, which creates and updates the tables on start. production on a live server, which runs the migrations on start and never rewrites the schema. |
PORT | The API's port, 8000. |
DB_TYPE | sqlite (what .env.example sets) or mysql. |
SQLITE_DATABASE | Path of the SQLite file, ./database.sqlite, relative to the folder the command runs in. |
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 dashboard's address, comma separated if there are several. Unset, it falls back to http://localhost:3030, never to *; required in production. |
FRONTEND_URL | Optional. The dashboard's address for the two live sockets (sign-in events and the notification bell). Unset, the sockets accept the same addresses as CORS_ORIGIN. |
TRUST_PROXY | Optional. How far the API believes X-Forwarded-For when counting sign-in attempts per visitor. Unset or auto reads it only from a proxy on a private address; false never; a number trusts exactly that many proxies. |
Generate your own JWT_SECRET with:
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"Any storage or AI credential below still holding its exact .env.example value counts as not set, so the feature it belongs to reports itself off instead of failing on its first call.
Use MySQL instead of SQLite
Create an empty database, then set the driver and the connection in back-end/.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 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 db:sync:prod or yarn seed:prod after yarn build), and yarn db:sync:prod again after an update that changes the schema. It keeps your data.
The template ships its migration for SQLite only. back-end/src/database/migrations/mysql/README.md has the one command that generates the MySQL one, if you want the API's start-up migrations on MySQL too.
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 this file, and restart or rebuild after changing one.
| Variable | What it does |
|---|---|
PORT | The port yarn dev and yarn start bind, 3030. Unset, it is still 3030. |
NEXT_PUBLIC_API_BASE_URL | The API's address including /api, for example http://localhost:8000/api. Setting it is what switches the sample data off; empty or missing, the dashboard runs on its sample data. |
NEXT_PUBLIC_WEBSOCKET_BASE_URL | The API's address without /api, for live notifications and permission updates. Optional: when empty it is taken from NEXT_PUBLIC_API_BASE_URL. |
NEXT_PUBLIC_MEDIA_HOSTNAME | Your media bucket's public host, the host of R2_PUBLIC_URL, without https://. Images from it are resized and compressed. Leave it empty until you have a bucket. |
NEXT_PUBLIC_DEMO_MODE | false. It must match DEMO_MODE in the API's .env; true is for a public showcase only. |
BUILD_STANDALONE | true makes yarn build emit a self-contained server, which the Dockerfile sets. Leave it unset otherwise. |
No AI provider key belongs here. Keys live in the API's .env only.
Docker Compose options
Nothing has to be set for a local run. To change something, put it in a .env file next to docker-compose.yml and run docker compose up --build again: the dashboard compiles these values in.
| Variable | What it does |
|---|---|
DASHBOARD_PORT, API_PORT | The ports on your computer: 3030 and 8000 by default. Set one when another program already uses that port, such as DASHBOARD_PORT=3040. The addresses below, CORS_ORIGIN and FRONTEND_URL follow them. |
DASHBOARD_URL, API_URL | Where the browser reaches each app. Set both when serving the stack on your own domains; the API's CORS_ORIGIN and FRONTEND_URL and the dashboard's API addresses are built from them. |
MEDIA_HOSTNAME | Your bucket's public host, with the R2_* variables in back-end/.env. |
SEED_DEMO_DATA | false starts with empty tables instead of the demo data. |
The API container also reads back-end/.env when it exists, so storage, AI 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: NODE_ENV=production, the port, SQLite at /data/database.sqlite, DEMO_MODE=false, CORS_ORIGIN and FRONTEND_URL. Without a JWT_SECRET of your own, the API generates one and keeps it in the data volume. The data lives in the kinora-data volume.
The API's Docker image
The image starts in production on SQLite at /data/database.sqlite, port 8000, with DEMO_MODE=false and SEED_DEMO_DATA=false, and allows browser calls from http://localhost:3030 (CORS_ORIGIN and FRONTEND_URL). Change any of them with -e on docker run, or pass your file with --env-file .env.
- On a fresh volume the container creates the tables, and adds the demo data only with
SEED_DEMO_DATA=true. An existing database is left alone. - Without a
JWT_SECRET, or with the example one, it generates a secret and keeps it in/data/.jwt-secret, so sign-ins survive a restart. - With
DB_TYPE=mysqlit creates nothing: runnode dist/database/sync-schema.jsornode dist/database/seeder.jsin the container once yourself.
Media storage
Profile pictures, progress photos, group covers and message attachments are uploaded to a Cloudflare R2 bucket, or any S3-compatible one. Set the five variables in back-end/.env and give the dashboard the bucket's public host through NEXT_PUBLIC_MEDIA_HOSTNAME (with Docker Compose, MEDIA_HOSTNAME).
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.devWithout a bucket, everything the seed writes is an /assets/images/… path that the dashboard serves from its own public/assets folder, so every screen renders with nothing uploaded. Only uploading new files stops working: an upload answers 400 until the bucket is set. Keep public/assets in the dashboard for as long as any row still points at it.
A file is at most 150 MB. The dashboard's uploader sends large files in parts of up to 16 MB through POST /api/helpers/upload-chunk, by itself.
The template ships no mail transport, so no message is ever emailed. The members screen's "resend verification email" answers without sending anything, and the forgot-password and reset-password pages are the screens only: no reset is sent, and the API has no route behind them. Connect your own mail provider, and add the reset routes, before going live if your members need either.
AI assistant
Included with your purchase. Sign in to read, or open it in your download.
Turning on the AI assistant: the provider keys and which models each one offers.
MCP server
Included with your purchase. Sign in to read, or open it in your download.
The key a coding agent sends to reach the assistant's tools.
Demo mode
Included with your purchase. Sign in to read, or open it in your download.
Running a public demo: the demo switch, the visitors' message allowance and the account limit.
Going live
Before you deploy anywhere public:
- Set
NODE_ENV=productionand a long randomJWT_SECRETof your own inback-end/.env. - Set
CORS_ORIGINto your dashboard's address, andFRONTEND_URLto the same address. - Keep
DEMO_MODE=falsein the API andNEXT_PUBLIC_DEMO_MODE=falsein the dashboard. - On an empty database, run
yarn build, thenyarn db:sync:prodonce (oryarn seed:prodfor the demo data), thenyarn start:prod. - Point your host's health check at
GET /api/health. It needs no token and is not rate limited. - Build the dashboard with
NEXT_PUBLIC_API_BASE_URLandNEXT_PUBLIC_WEBSOCKET_BASE_URLat your deployed API. - Change the seeded passwords, or start from empty tables.
Not for a database with real members
yarn setup:fresh builds, drops every table and reseeds, every time it runs. Use it to stand an environment up from nothing, never as the build or start command of a live product.
To drop the demo code entirely, run yarn remove:demo in both apps, on a clean tree: it cannot be undone without version control. yarn remove:mock in the dashboard deletes the sample data.