Backend — Environment & Configuration
Installing dependencies, .env setup, and database initialization
Step 1 — Install Backend Dependencies
In the VS Code terminal with saas-pos-backend open, run:
This downloads all required packages. It takes 1–3 minutes. Wait for the cursor to return.
Step 2 — Create the .env File
The repository ships a documented template. Copy it and fill in your values:
(On Windows without Git Bash: duplicate .env.example in the VS Code sidebar
and rename the copy to .env.)
Every variable is validated at startup (Zod, in src/config/index.ts). A
missing or malformed required value stops the server with a descriptive
error, so a typo can't cause silent misbehavior.
Full .env Variable Reference
Only DATABASE_URL, JWT_ACCESS_SECRET, and JWT_REFRESH_SECRET are strictly
required to boot — everything else has a sensible default — but you should set
the first block explicitly for a working local install.
| Variable | Description | Recommended Dev Value | |
|---|---|---|---|
| NODE_ENV | Runtime mode: development, production, or test | development | Optional |
| PORT | Backend HTTP port. Use 5000 so it matches the frontend's default NEXT_PUBLIC_API_URL (the frontend itself runs on 3000). | 5000 | Required |
| DATABASE_URL | PostgreSQL connection string | postgresql://postgres:pass@localhost:5432/posvelo?schema=public | Required |
| REDIS_URL | Redis connection string (sessions, rate limiting, job queues) | redis://localhost:6379 | Required |
| JWT_ACCESS_SECRET | Access-token signing secret — minimum 32 characters | 64-char hex string | Required |
| JWT_REFRESH_SECRET | Refresh-token signing secret — minimum 32 characters, different value | 64-char hex string | Required |
| JWT_ACCESS_EXPIRY | Access token lifetime | 15m | Optional |
| JWT_REFRESH_EXPIRY | Refresh token lifetime | 7d | Optional |
| CORS_ORIGINS | Comma-separated list of allowed browser origins — set to your frontend URL | http://localhost:3000 | Required |
| SETUP_ACCESS_CODE | One-time code that unlocks the first-run setup wizard (see First-Time Setup) | any strong random string | Required |
| SUPER_ADMIN_EMAIL | Email for the platform Super Admin created by npm run db:seed:super-admin | owner@yourdomain.com | Optional |
| SUPER_ADMIN_PASSWORD | Password for that Super Admin account | a strong password | Optional |
| LOG_LEVEL | Pino log verbosity (debug, info, warn, error) | debug | Optional |
| DEMO_MODE | Public-demo sandbox: periodic data resets, destructive actions blocked. Leave false. | false | Optional |
| BCRYPT_ROUNDS | Legacy bcrypt cost factor (argon2 is the primary hasher) | 12 | Optional |
| RATE_LIMIT_MAX | Max requests per rate-limit window | 100 | Optional |
| RATE_LIMIT_WINDOW_MS | Rate-limit window in milliseconds | 60000 | Optional |
| MASTER_ENCRYPTION_KEY | Key for encrypting stored third-party credentials — set in production | 64-char hex string | Optional |
The CORS variable is CORS_ORIGINS (not FRONTEND_URL
), and the token lifetimes are JWT_ACCESS_EXPIRY /
JWT_REFRESH_EXPIRY (not …_EXPIRES_IN). Cloudinary
credentials belong in the frontend .env.local, not here —
the backend never talks to Cloudinary. Prisma uses a single
DATABASE_URL; there is no DIRECT_URL.
Optional — Billing (Stripe)
These are only needed for the v2.1 platform features. Leave them blank for a plain install: with no Stripe keys, free / $0 plans still work.
| Variable | Description | Example | |
|---|---|---|---|
| STRIPE_SECRET_KEY | Stripe secret key — enables paid billing | sk_test_… / sk_live_… | Optional |
| STRIPE_WEBHOOK_SECRET | Signing secret for the Stripe webhook | whsec_… | Optional |
| STRIPE_PUBLISHABLE_KEY | Stripe publishable key | pk_test_… / pk_live_… | Optional |
| STRIPE_CURRENCY | ISO 4217 billing currency for plan pricing | usd | Optional |
For the billing setup see the dedicated guide: Subscriptions & Billing (Stripe setup, plans, and plan limits).
Generating Secure Secrets
Run this command three times in any terminal. Use the outputs for
JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, and (in production)
MASTER_ENCRYPTION_KEY. The two JWT secrets must be different values.
Getting Your PostgreSQL Database URL
Docker (ships with the project)
The backend includes a docker-compose.yml that starts PostgreSQL 16 and
Redis 7 locally. Set a password and start both:
Then use:
Neon.tech (hosted)
- Go to neon.tech and create a free account. Click Create Project.
- After creation, click Connection Details and copy the Connection String.
- It looks like:
postgresql://user:pass@host.neon.tech/dbname?sslmode=require - Use this exact string for
DATABASE_URL.
Local PostgreSQL
Download from postgresql.org/download and install with defaults. Create a database named posdb:
Your local URL will be:
Getting Your Redis URL
If you used the project's Docker compose above, Redis is already running at
redis://localhost:6379. Otherwise:
Upstash (hosted)
Go to upstash.com, create a free account, and create a Redis database. Copy the Redis URL from the dashboard — it starts with rediss://.
Local Redis
- Windows: Install Memurai from memurai.com
- macOS:
brew install redisthenbrew services start redis - Linux:
sudo apt install redis-serverthensudo systemctl start redis - Local URL:
redis://localhost:6379
Step 3 — Database Initialization
Run these commands in order in your backend terminal. Do not skip any.
| Command | What It Does |
|---|---|
npm run db:generate | Generates the Prisma client — the type-safe database access layer |
npm run db:migrate | Creates all tables in the database. When prompted for a name, enter: initial_setup |
npm run db:seed | Populates full demo data: a demo tenant with stores, products, customers, employees, and sample sales |
npm run db:seed:super-admin | Creates the platform Super Admin account from SUPER_ADMIN_EMAIL / SUPER_ADMIN_PASSWORD in your .env |
Run db:migrate only once on a fresh database. For future code updates
use npm run db:migrate:prod — it applies only new changes without prompts or
data loss. db:seed:super-admin requires SUPER_ADMIN_EMAIL and
SUPER_ADMIN_PASSWORD to be set in .env first; it prints a clear error if
they are missing. If you prefer to start with an empty database (no demo
data), skip db:seed and create your first business through the setup wizard
instead — see First-Time Setup.