A lightweight, web-based prepaid tab system for a single group. Load a prepaid balance, sell products against it, and watch the remaining balance, consumption, and profit update in real time. Built for a small hotel / guesthouse setting where one group runs a shared tab over a stay — group total only, no per-person tracking.
“Adisyon” is Turkish for a bar/restaurant tab or check.
- Sales (
/) — Category-grouped product cards, a live cart, the remaining balance, and a low-balance warning. If a cart exceeds the balance it warns but still allows the sale (the balance is allowed to go negative). - Products (
/products) — Add / edit products (name, category, cost, price, optional stock). Products are never hard-deleted — they're deactivated, so historical sales stay intact. - Balance (
/balance) — Top up the prepaid balance (cumulative), browse top-up history, and configure settings (group size + low-balance threshold). - Stats (
/stats) — Day / week / month / all-time ranges: revenue, cost, profit, per-person average, top products, category breakdown, a daily revenue trend, and voided sales. - Auth — A single shared password protects the whole app via a signed, HTTP-only cookie. A lightweight proxy guards every page and API route.
Next.js 16 (App Router) · TypeScript · Prisma 6 + PostgreSQL · Tailwind CSS v4 + shadcn/ui (Base UI primitives) · Zod. Fully responsive and mobile-first — a bottom-sheet cart on phones, a sticky sidebar on desktop.
- Money is never a float. All amounts are stored as
Decimal(10,2)(Turkish Lira, ₺). Authoritative sums are computed in SQL and returned as exact decimal strings, never passing through a JavaScript float. - Derived values aren't stored. Remaining balance, revenue, and profit are always queried — never cached in a column that could drift.
- Sales snapshot price & cost. Each sale copies the product's price and cost at the moment of sale, so changing a product's price later never rewrites the profit of past sales.
- Stock is optional per product. Leave stock empty and it's untracked; otherwise it decrements on sale and is restored on void.
Requires Node 20+ and Docker.
# 1) Install dependencies
npm install
# 2) Start local PostgreSQL (host port 5434)
docker compose up -d
# 3) Environment variables
cp .env.example .env # DATABASE_URL is ready; set APP_PASSWORD and AUTH_SECRET
# 4) Schema + sample data
npm run db:migrate # apply migrations
npm run db:seed # sample products + default settings
# 5) Run
npm run dev # http://localhost:3000 (password: .env > APP_PASSWORD)| Script | Description |
|---|---|
npm run dev |
Development server |
npm run build / npm start |
Production build / start |
npm run db:migrate |
prisma migrate dev |
npm run db:seed |
Load the sample data |
npm run db:studio |
Browse the data in Prisma Studio |
npm run db:reset |
Reset the DB + migrate + seed |
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
APP_PASSWORD |
The single shared password to sign in |
AUTH_SECRET |
Long random string used to sign the session cookie |
Ship it as a Docker image — the repo includes a production Dockerfile. On every
deploy it applies pending migrations (prisma migrate deploy) and then starts
the server on port 3000.
It runs on any Docker host. The steps below are for Coolify, which is what it was built for:
- Database: create a PostgreSQL resource and copy its internal connection URL.
- Application: connect this repo and choose Build Pack: Dockerfile. (The
bundled
docker-compose.ymlis for local development only — don't use it in production.) - Environment variables:
DATABASE_URL— the Postgres internal URLAPP_PASSWORD— login password (pick a strong one)AUTH_SECRET— long random string:openssl rand -base64 32
- Port & domain: port
3000is auto-detected. Assign a domain; HTTPS is required in production because the session cookie isSecure. - Health check (optional): path
/api/health. - Deploy. Migrations run automatically on first boot once the DB is reachable.
- Sample data (optional): migrations create the schema, but seeding does
not run automatically in production. Run
npm run db:seedonce from the app shell for the demo products, or just add products in the UI. - Sign in with
APP_PASSWORD.
Run a single instance — multiple concurrent instances could race on migrations.
- Currency & locale. Amounts are Turkish Lira (₺), formatted in
en-US; dates use theEurope/Istanbultime zone. These are centralized inlib/money.ts,lib/datetime.ts, andlib/stats.tsif you want to change them. - Prisma is pinned to v6. v7 drops
url = env()fromschema.prismaand requires aprisma.config.ts+ a driver adapter; unnecessary surface area for a tool this size, so the upgrade was deliberately skipped.
MIT © 2026 HFerrahoglu