Link clocked time to invoice items with bidirectional sync, add entry editing on web, broaden session cookie detection for Expo clients, and handle API rate limits without signing users out. Co-authored-by: Cursor <cursoragent@cursor.com>
beenvoice-web
Web application and API for beenvoice — invoicing for freelancers and small businesses. Includes the Next.js dashboard, tRPC API, better-auth, PostgreSQL persistence, PDF/email delivery, time tracking, and an MCP automation endpoint.
Repository: git.soconnor.dev/soconnor/beenvoice-web
Architecture: docs/ARCHITECTURE.md
Mobile companion: beenvoice-app (separate repo; often checked out beside this one in a workspace)
Stack
| Layer | Technology |
|---|---|
| App | Next.js 16 App Router, React 19 |
| API | tRPC 11 + SuperJSON |
| Database | PostgreSQL 17, Drizzle ORM |
| Auth | better-auth (email/password, optional Authentik OIDC, Expo mobile) |
| UI | shadcn/ui, Tailwind CSS v4 |
| Email / PDF | Resend, @react-pdf/renderer |
| Runtime | Bun |
Features
- Clients, businesses, invoices (line items, tax, status workflow)
- Time clock with one running timer per user; clock-out can append invoice lines
- Expenses, payments, recurring invoices, invoice templates
- PDF export and email delivery (Resend)
- Public invoice links (
/i/[token]) - CSV import, reports, platform branding / admin settings
- MCP API (
/api/mcp) for automation via API keys (bv_…) - Optional Authentik OIDC SSO
Prerequisites
- Bun 1.x
- Docker & Docker Compose (for PostgreSQL locally or full-stack deploy)
- Git
Local development
1. Clone and install
git clone https://git.soconnor.dev/soconnor/beenvoice-web.git
cd beenvoice-web
bun install
2. Environment
cp .env.example .env.local
Edit .env.local for local dev. Minimum:
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
DB_DISABLE_SSL=true
AUTH_SECRET=your-dev-secret # openssl rand -base64 32
BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000
Email and SSO are optional for local work — leave RESEND_* and AUTHENTIK_* blank unless you need them.
3. Database
Start Postgres (dev compose exposes port 5432):
docker compose -f docker-compose.dev.yml up -d
After a fresh volume (docker compose down -v), Postgres starts empty — you must apply schema before registering or signing in.
Apply schema (pick one):
bun run db:push # fast iteration during development
# bun run db:migrate # same migrations the Docker image runs in production
Demo account. For App Store review and local testing, bun run db:migrate applies 0014_seed_demo_account.sql, which creates a pre-populated user (db:push does not). Sign in at /auth/login:
- Email:
demo@example.com - Password:
demo123
The account includes a sample business, clients, and invoices (draft, sent, and paid).
4. Run
bun run dev
Open http://localhost:3000, register at /auth/register, or sign in with the demo account above.
Docker deployment (app + database)
The production compose file runs the Next.js app and PostgreSQL.
Container startup runs bun migrate.ts && bun run start (see Dockerfile). Drizzle only applies pending migrations — safe to run on every restart; already-applied migrations are skipped.
The Docker build runs next build on Node 22 (Bun can crash on Linux arm64 during the page-data worker phase). The runtime image still uses Bun for migrations and next start. docker-compose.yml does not set container memory or CPU limits — containers can use whatever the Docker host provides.
1. Configure
cp .env.example .env
Set at least:
AUTH_SECRET=<openssl rand -base64 32>
BETTER_AUTH_URL=https://your-public-hostname
NEXT_PUBLIC_APP_URL=https://your-public-hostname
BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL must match the URL users actually use in the browser (scheme + host + port). If they point at localhost but you access the app via another hostname, sign-up and sign-in will fail (often with a vague "REQUIRED" toast).
NEXT_PUBLIC_* values are embedded at image build time. Rebuild after changing NEXT_PUBLIC_APP_URL, white-label defaults, or NEXT_PUBLIC_AUTHENTIK_ENABLED:
docker compose build --no-cache app
BETTER_AUTH_URL and AUTH_SECRET are read at container runtime from .env — you can change them without rebuilding, then restart the app container.
2. First start (or after code changes)
./scripts/docker-deploy.sh
# or: bun run docker:deploy
# or: docker compose up -d --build
--build is required after code changes. A plain docker compose up -d reuses the existing beenvoice:local image and does not pick up new code from git pull. The deploy script tags the image with the current git SHA (beenvoice:<sha>) so each deploy gets a distinct image.
App listens on ${WEB_PORT:-${PORT:-3000}} on the host (container port is always 3000). Postgres stays on the internal compose network.
3. Updating an existing deploy
git pull
./scripts/docker-deploy.sh # recommended: rebuild + tag with git SHA + restart
# or: docker compose up -d --build
| Command | New code? | Migrations run? |
|---|---|---|
git pull only |
No | No |
docker compose up -d (no --build) |
No — reuses beenvoice:local |
Only if the app container restarts (same image) |
./scripts/docker-deploy.sh or docker compose up -d --build |
Yes | Yes — on app container start |
docker compose restart app |
No | Yes — migrate runs again (no-op if up to date) |
Prune old app images occasionally: docker image prune -f (or remove specific beenvoice:* tags).
To verify migration files match the journal before deploy: bun run db:verify-journal.
Coolify
For self-hosted Coolify deploys (especially ENOTFOUND minio with Application + separate MinIO compose), see docs/COOLIFY.md. Recommended: deploy docker-compose.coolify.yml as a single Compose resource.
4. Sign-ups
Registration is enabled by default. To block new email/password accounts:
DISABLE_SIGNUPS=true
Use the literal strings true or false (or omit the variable). Do not rely on bare boolean coercion from shell/compose — the app parses these explicitly.
5. Optional services
| Variable | Purpose |
|---|---|
RESEND_API_KEY, RESEND_DOMAIN |
Invoice and password-reset email |
AUTHENTIK_ISSUER, AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET |
OIDC SSO (also set NEXT_PUBLIC_AUTHENTIK_ENABLED=true and rebuild) |
CRON_SECRET |
Protects /api/cron/generate-recurring |
DISABLE_SIGNUPS=true |
Block new registrations |
Project structure
beenvoice-web/
├── src/app/ # Routes (dashboard, auth, /api/*)
├── src/server/api/ # tRPC routers
├── src/server/db/ # Drizzle schema, pool, migrate.ts
├── src/components/ # UI (ui/, forms/, layout/, branding/)
├── src/lib/ # auth, PDF, email, branding helpers
├── drizzle/ # SQL migrations
├── Dockerfile # Production image (migrate + next start)
├── docker-compose.yml # App + Postgres + MinIO (deploy)
├── docker-compose.coolify.yml # Coolify Compose (app + db + minio)
├── docker-compose.coolify-minio.yml # MinIO-only for Coolify Application pairing
├── docker-compose.dev.yml # Postgres only (local dev)
└── docs/ # Architecture and UI guides
See docs/ARCHITECTURE.md for routers, schema, auth flows, and MCP.
Scripts
# App
bun run dev # next dev --turbo
bun run build # production build
bun run start # next start
bun run check # eslint + tsc
# Database
bun run db:push # push schema (local dev)
bun run db:migrate # run drizzle migrations
bun run db:generate # generate new migration SQL
bun run db:studio # Drizzle Studio
# Formatting
bun run lint
bun run lint:fix
bun run format:write
bun run typecheck
# Docker helpers
bun run docker:up # dev Postgres only (Colima + docker-compose.dev.yml)
bun run docker:down # stop dev Postgres + colima
bun run docker:deploy # production: rebuild app image + docker-compose.yml up -d
Full-stack deploy uses bun run docker:deploy or ./scripts/docker-deploy.sh (see Docker deployment), not bun run docker:up.
API surface
| Endpoint | Auth | Purpose |
|---|---|---|
/api/trpc |
Session cookie or API key | Primary API (web + mobile) |
/api/auth/* |
Varies | better-auth + custom register/reset REST |
/api/mcp |
API key only | JSON-RPC automation tools |
/i/[token] |
Public token | Client invoice view |
/api/i/[token]/pdf |
Public token | Invoice PDF download |
Business logic lives in src/server/api/routers/ with Zod validation.
Customization
- Runtime branding: Dashboard → Administration (platform settings)
- Build-time defaults:
NEXT_PUBLIC_BRAND_*in.env(rebuild Docker image to apply) - Theme / fonts:
src/styles/globals.css, appearance settings in the app - Logo component:
src/components/branding/logo.tsx
Documentation
| Doc | Contents |
|---|---|
| docs/ARCHITECTURE.md | Stack, routers, schema, auth, Docker, MCP |
| docs/COOLIFY.md | Coolify deploy paths and MinIO networking |
| docs/README.md | Index of UI and product guides |
| AGENTS.md | Conventions for AI-assisted development |
License
MIT — see LICENSE.
