10 KiB
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.
Architecture: docs/ARCHITECTURE.md
Mobile companion: apps/mobile
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. Install the workspace
cd beenvoice
bun install
cd apps/web
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):
bun run docker:up
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 creates a pre-populated demo@example.com account (db:push does not) with the public password demo123.
To rotate the credential temporarily, provision a private password:
DEMO_ACCOUNT_PASSWORD='<private 12+ character password>' bun run demo:provision
Provisioning rotates the credential and invalidates prior sessions. Do not commit or publish a private replacement password. 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 the web migration script followed by bun run start (see the root 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. The root docker-compose.yml does not set container memory or CPU limits.
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 -f ../../docker-compose.yml 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, from the repository root: 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.
Scheduled recurring invoices
The app container does not run a cron daemon. It starts the web server with
bun migrate.ts && bun run start, and recurring invoice generation only happens
when something calls POST /api/cron/generate-recurring with
Authorization: Bearer $CRON_SECRET.
- Coolify deploys: use a Coolify scheduled task to call the endpoint.
- Full Docker deploys: use host cron, a small scheduler sidecar, or an
external scheduler to call
http://localhost:${WEB_PORT:-${PORT:-3000}}/api/cron/generate-recurring.
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 root 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 garage with Application + separate Garage compose), see docs/COOLIFY.md. Recommended: deploy the root 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
apps/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
└── docs/ # Architecture and UI guides
Workspace configuration, Dockerfiles, Compose files, and the shared packages/domain package live at the repository root.
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 Garage networking |
| docs/README.md | Index of UI and product guides |
| AGENTS.md | Conventions for AI-assisted development |
License
MIT — see the root LICENSE.
