Skip to the article
Aniq-UI

KinoraEnvironment variables

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

FileRead byHolds secrets
back-end/.envThe API, with yarn dev and inside Docker ComposeYes. Never commit it.
dashboard/.envThe dashboard, at build timeNo. Every value is public.
.env beside docker-compose.ymlDocker Compose, for the full stackNo

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.

VariableWhat it does
NODE_ENVdevelopment 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.
PORTThe API's port, 8000.
DB_TYPEsqlite (what .env.example sets) or mysql.
SQLITE_DATABASEPath of the SQLite file, ./database.sqlite, relative to the folder the command runs in.
JWT_SECRETSigns every sign-in. Required. The example value is accepted outside production only.
JWT_EXPIRATIONHow long a sign-in lasts, 7d by default.
CORS_ORIGINThe dashboard's address, comma separated if there are several. Unset, it falls back to http://localhost:3030, never to *; required in production.
FRONTEND_URLOptional. 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_PROXYOptional. 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:

Terminal
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:

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-name

Then 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.

VariableWhat it does
PORTThe port yarn dev and yarn start bind, 3030. Unset, it is still 3030.
NEXT_PUBLIC_API_BASE_URLThe 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_URLThe 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_HOSTNAMEYour 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_MODEfalse. It must match DEMO_MODE in the API's .env; true is for a public showcase only.
BUILD_STANDALONEtrue 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.

VariableWhat it does
DASHBOARD_PORT, API_PORTThe 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_URLWhere 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_HOSTNAMEYour bucket's public host, with the R2_* variables in back-end/.env.
SEED_DEMO_DATAfalse 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=mysql it creates nothing: run node dist/database/sync-schema.js or node dist/database/seeder.js in 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).

back-end/.env
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.dev

Without 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.

Email

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:

  1. Set NODE_ENV=production and a long random JWT_SECRET of your own in back-end/.env.
  2. Set CORS_ORIGIN to your dashboard's address, and FRONTEND_URL to the same address.
  3. Keep DEMO_MODE=false in the API and NEXT_PUBLIC_DEMO_MODE=false in the dashboard.
  4. On an empty database, run yarn build, then yarn db:sync:prod once (or yarn seed:prod for the demo data), then yarn start:prod.
  5. Point your host's health check at GET /api/health. It needs no token and is not rate limited.
  6. Build the dashboard with NEXT_PUBLIC_API_BASE_URL and NEXT_PUBLIC_WEBSOCKET_BASE_URL at your deployed API.
  7. 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.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.