Skip to the article
Aniq-UI

NorthstackEnvironment variables

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

FileRead byHolds secrets
back-end/.envThe API, with yarn dev and inside Docker ComposeYes. Never commit it.
website/.envThe website: the NEXT_PUBLIC_* values at build time, API_INTERNAL_URL on its serverNo. Every value is public or an address.
dashboard/.envThe dashboard, at build time, and its PORT when yarn dev or yarn start beginsNo. Every value is public.
.env beside docker-compose.ymlDocker Compose only: the ports and the addresses it builds the three apps withNo.

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, and every value in it is optional. The NEXT_PUBLIC_* values are read when the website is built and are public, so they never hold a secret; API_INTERNAL_URL is read by the website's own server. Without the file, the site runs on its built-in sample data. To use an API, create it from .env.example, which points at one on http://localhost:8000: cp .env.example .env. With Docker, pass the NEXT_PUBLIC_* values as --build-arg and API_INTERNAL_URL with -e.

Where the settings live

One file, .env in the dashboard's folder, read when the dashboard is built and, for PORT, when yarn dev or yarn start begins. Every value in it is public, so it never holds a secret: provider and payment keys live in the API's .env. Without the file, the dashboard runs on its built-in sample data. To use an API, create it from .env.example, which points at one 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, yarn start:prod and yarn seed. 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: NODE_ENV=production, SQLite at /data/database.sqlite, uploads in /data/uploads, CORS_ORIGIN for the two local frontends, FRONTEND_URL and DASHBOARD_URL set to http://localhost:3031, WEBSITE_URL=http://localhost:3030, PUBLIC_API_URL=http://localhost:8000 and PUBLIC_MEDIA_URL=http://localhost:8000/media. 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 beginning The API cannot start: that says what to fix.

VariableWhat it does
NODE_ENVdevelopment locally, which creates and updates the tables on start. production on a live server, which never touches the tables and refuses the example JWT_SECRET and an empty CORS_ORIGIN.
PORTThe API's port, 8000. Both frontends point at it.
DB_TYPEsqlite (the default when unset) or mysql.
SQLITE_DATABASEPath of the SQLite file, ./database.sqlite, relative to the API's folder.
JWT_SECRETSigns every sign-in. Required. The example value is accepted outside production only.
JWT_EXPIRATIONHow long a sign-in lasts, 7d when unset. The dashboard's sign-in form overrides it: Remember me ticked signs a 30-day sign-in, unticked a 12-hour one.
CORS_ORIGINThe 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_URLThe dashboard's address, the one origin its live permission updates accept, http://localhost:3031 when unset.
DASHBOARD_URLThe dashboard's address in the links the API writes: invitations, password resets and the page a plan checkout returns to. http://localhost:3031 when unset.
WEBSITE_URLThe website's address: Stripe and PayPal send licence buyers back to it. http://localhost:3030 when unset.
PUBLIC_API_URLThe API's own public address, used in the signed invoice download links.
RATE_LIMIT_LOGINFailed sign-ins one address may make in 15 minutes before it gets 429, 10 by default.
TRUST_PROXYThe number of reverse proxies in front of the API (1 behind Nginx, Caddy or a load balancer; 2 with Cloudflare in front of that proxy), so the sign-in limit and the demo's per-address limits count visitors rather than a proxy. The address is read from the right of X-Forwarded-For, so a client cannot choose it. Leave it empty when the API is reached directly.
ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_NAMEThe first Platform admin that yarn admin:create makes, for a start without the sample accounts. The command asks for an empty email or password; the name is Platform Admin when empty.
SEED_SAMPLE_DATADocker only. true (the default) seeds a new volume with the sample data and the published sign-ins. false starts it empty: the tables, plus the first Platform admin when ADMIN_EMAIL and ADMIN_PASSWORD are set. With Docker Compose it goes in the .env file next to docker-compose.yml, with ADMIN_EMAIL and ADMIN_PASSWORD; Compose passes these three from there, so in back-end/.env they are read only by the yarn commands.

Generate your own JWT_SECRET with:

Terminal
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"

Every optional feature below is off while its variables in .env.example stay empty.

Payments

Two flows take money. The dashboard's Billing page changes a workspace's subscription plan, and the website's checkout sells the licences. Both take card details only on Stripe's or PayPal's own page (a plan upgrade is charged to the card Stripe already holds), so they are never typed on your site, and both work with no account at all through built-in test checkouts that charge nobody. Use test keys first: they move no money.

The API's .env
STRIPE_SECRET_KEY=sk_test_your_key
STRIPE_WEBHOOK_SECRET=whsec_your_secret
BILLING_PROVIDER=
BILLING_GRACE_DAYS=7
PAYPAL_CLIENT_ID=your-client-id
PAYPAL_CLIENT_SECRET=your-client-secret
PAYPAL_ENV=sandbox
VariableWhat it does
STRIPE_SECRET_KEYStripe, for both flows. With it, a first payment opens Stripe Checkout, an upgrade of a running subscription is charged to the card on file, a downgrade is scheduled on the subscription, and Manage payment method opens the Stripe customer portal, and a card on the licence checkout pays on Stripe's hosted page. A key starting sk_test_ is test mode; with a sk_live_ key plan changes charge real cards.
STRIPE_WEBHOOK_SECRETVerifies the one webhook both flows share, https://<your-api>/api/billing/webhooks/stripe, with the events checkout.session.completed, checkout.session.expired, invoice.paid, invoice.payment_failed and customer.subscription.deleted. Each event is applied once. With STRIPE_SECRET_KEY set and this empty, the API's log says so at start and every delivery is refused with a 400 naming the variable, so renewals, failed payments and cancellations are not recorded.
BILLING_PROVIDEREmpty picks Stripe when its key is set and the test checkout otherwise. simulator forces the test checkout even with keys.
BILLING_GRACE_DAYSDays a past-due workspace, or one whose trial ended unpaid, may still start new projects, invitations and API keys, 7 by default. Reads always keep working, and nothing is deleted for a failed payment.
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRETA PayPal REST app, for PayPal on the licence checkout. The payment is captured when the buyer returns.
PAYPAL_ENVsandbox (the default, no real money) or live.

On your own computer Stripe cannot reach the webhook, so the return pages ask Stripe for the session themselves and a test payment still completes. A plan or a licence counts as paid only once the API has verified it with the provider, never from the return address alone. Northstack charges no tax by itself, so set up tax for your country before taking real payments.

Email

Invitations, password resets, licence receipts, the billing emails and the weekly digest are sent over SMTP once SMTP_HOST is set, each in the recipient's language (the workspace's when they have chosen none). Until then every message is written to the API's log instead, links included, so a local install can follow them (with Docker: docker compose logs api).

The API's .env
SMTP_HOST=smtp.your-provider.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-smtp-user
SMTP_PASSWORD=your-smtp-secret
SMTP_FROM=Northstack <no-reply@your-domain.com>
CONTACT_INBOX=hello@your-domain.com

SMTP_SECURE=true is for a server that speaks TLS from the first byte, usually on port 465. CONTACT_INBOX is where the website's contact form messages are forwarded; empty keeps them in the database only.

Use MySQL instead of SQLite

Create an empty database, then set the driver and the connection in the API's .env:

The API's .env
DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=your-database-secret
DB_DATABASE=northstack

Then run yarn seed, which creates the tables and the sample data. The running API only creates and updates the tables itself when NODE_ENV=development, so with NODE_ENV=production run yarn admin:create:prod once after yarn build for the tables and your first Platform admin (yarn seed:prod for a sample install), and yarn schema:prod after each update. The Docker image creates the database by itself on SQLite only: on MySQL, run node dist/database/create-admin.js (or node dist/database/seeder.js) once in the container yourself.

Uploads and media

The API serves the kit's brand files itself at /media. Project files and pictures uploaded from the dashboard, up to 10 MB each, are stored on the API's disk under MEDIA_UPLOAD_DIR and served at /media/uploads/…, unless a bucket is set.

The API's .env
PUBLIC_MEDIA_URL=http://localhost:8000/media
MEDIA_UPLOAD_DIR=./public/media/uploads

PUBLIC_MEDIA_URL is the address /media is reached at from a browser. On a server, point MEDIA_UPLOAD_DIR at persistent storage; with Docker it is in the data volume, and the container moves a relative folder there by itself. The API sends every file there with nosniff, and a file that is not a picture, a PDF, plain text, audio or video as a download.

To send uploads to a Cloudflare R2 bucket, set all five variables. With fewer than five, uploads stay on the disk. The API's log names the storage it uses when it starts: Media storage: R2 bucket followed by the bucket's name, or Media storage: local folder followed by the folder.

The API's .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

Website and 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. With Docker, pass them as --build-arg.

NEXT_PUBLIC_API_BASE_URL decides where each frontend's data comes from. Empty, or with no .env at all, the app runs on its built-in sample data and calls no API. Set to the API's address including /api (http://localhost:8000/api in .env.example and in the Docker images), it uses that API, and stands in with the sample data in the browser only while that API does not answer.

The website's settings

VariableDefaultWhat it does
NEXT_PUBLIC_SITE_URLhttp://localhost:3030This site's public address: canonical links, language alternates, the sitemap and the sharing cards are built from it. Set it to your domain before you go live.
NEXT_PUBLIC_API_BASE_URLEmpty without .envThe Northstack API, including /api, for the contact form and the licence checkout.
API_INTERNAL_URLEmptyOptional, read at run time by the website's server only, for the checkout and its receipt rendered on the server. Empty uses NEXT_PUBLIC_API_BASE_URL. In Docker, set it to the API's address as the container reaches it (http://api:8000/api in Docker Compose).
NEXT_PUBLIC_DASHBOARD_URLhttp://localhost:3031The dashboard. Live demo, Explore the demo, See role controls and Sign in on /error/401 open it, and the site's /auth/signin, /auth/signup and /auth/forgot-password hand over to its pages.
NEXT_PUBLIC_PURCHASE_URL_SINGLE, NEXT_PUBLIC_PURCHASE_URL_STUDIOEmptyAn outside store's checkout page for each licence. Empty keeps the site's own checkout at /checkout.
NEXT_PUBLIC_SAMPLE_DATA_NOTICEEmptyalways shows the Sample data notice in a production build while the API set above is not answering; with Docker, --build-arg NEXT_PUBLIC_SAMPLE_DATA_NOTICE=always. It always shows in development, and in every build made with no API.
BUILD_STANDALONEEmptytrue makes yarn build emit a self-contained server in .next/standalone, which the Dockerfile sets. Leave it empty for yarn start.

The website's port is 3030, written in its package.json scripts. For another, run yarn next dev -p 3040 (or yarn next start -p 3040 after yarn build).

The dashboard's settings

VariableDefaultWhat it does
PORT3031The port yarn dev and yarn start use. scripts/next-with-port.mjs reads it from .env and hands it to Next.js.
NEXT_PUBLIC_API_BASE_URLEmpty without .envThe Northstack API, including /api. Every screen talks to it.
NEXT_PUBLIC_WEBSOCKET_BASE_URLThe API's address without /apiThe API server, for live permission updates.
NEXT_PUBLIC_BASE_URLhttp://localhost:3031This dashboard's public address: robots.txt, the sitemap, the workspace address shown in onboarding and the absolute metadata addresses.
NEXT_PUBLIC_WEBSITE_URLhttp://localhost:3030The public website: the sidebar's Documentation link and the sign-in page's Privacy and Terms links.
BUILD_STANDALONEUnsettrue makes yarn build emit a self-contained server, which the Dockerfile sets. Leave it unset otherwise.

No AI provider or payment key belongs here: a NEXT_PUBLIC_* value is visible to anyone who opens the page. A provider the API has no key for can still be used with a key the reader adds in the assistant itself.

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.

VariableDefaultWhat it does
SITE_PORT3030The website's port on your computer.
ADMIN_PORT3031The dashboard's port on your computer.
API_PORT8000The API's port on your computer.
SITE_URL, ADMIN_URL, API_URLThe local addresses on those portsWhere the browser reaches each app. Set all three when serving the stack on your own domains, such as API_URL=https://api.your-domain.com.
PURCHASE_URL_SINGLE, PURCHASE_URL_STUDIOEmptyAn outside store's checkout for each licence, if you sell through one; empty keeps the website's own checkout.
SEED_SAMPLE_DATAtrueRead on a new volume's first start only: true seeds the sample data, false starts it empty.
ADMIN_EMAIL, ADMIN_PASSWORDEmptyWith SEED_SAMPLE_DATA=false, the first Platform admin created on the first start; empty creates the tables only.

Change a port when another program already uses it, such as ADMIN_PORT=3041. The API's CORS_ORIGIN, FRONTEND_URL, DASHBOARD_URL, WEBSITE_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 also reads back-end/.env when it exists, so the optional keys (Stripe, PayPal, mail, R2, AI, MCP) are set in one place for both yarn dev and Docker. docker-compose.yml wins for the values that differ inside a container: NODE_ENV=production, the port, the database path, the upload folder and the addresses above. Without a JWT_SECRET of your own, or with the example one, the API generates one and keeps it in the data volume. The database and the uploads live in the northstack-data volume.

AI assistant keys

Included with your purchase. Sign in to read, or open it in your download.

Turning on the workspace assistant on the server: the provider keys and who pays for each message.

The MCP server

Included with your purchase. Sign in to read, or open it in your download.

Letting coding agents use the workspace tools: the key, the workspace 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 product.

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.