Files
beenvoice/apps/web
2026-08-17 16:35:14 -04:00
..
2026-08-17 16:35:14 -04:00
2026-08-17 13:54:45 -04:00
2026-08-17 16:35:14 -04:00
2026-08-17 16:35:14 -04:00
2026-08-17 16:35:14 -04:00
2026-08-17 16:35:14 -04:00

beenvoice Logo

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 or SMTP/Mailpit, @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 in production, Mailpit locally)
  • 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

SSO is optional for local work. Email defaults to Mailpit: start the development Compose services and open http://localhost:8028 to inspect messages.

bun run --filter @beenvoice/web docker:up
bun run email:preview

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 Compose stack includes a dedicated PostgreSQL-backed worker. It discovers due recurring invoices every minute, enqueues idempotent jobs, and processes them with bounded retries and stale-lock recovery. No Coolify scheduled task or Redis service is required.

POST /api/cron/generate-recurring remains available as an optional authenticated "schedule now" hook. It only enqueues due work; invoice generation stays in the worker.

Scheduled invoice delivery

The web and mobile send screens can enqueue invoice email for a future date and time. Each job stores an absolute instant and the originating IANA timezone, supports rescheduling or cancellation before the worker claims it, and uses a Resend idempotency key during bounded retries. The worker reaches the web app at APP_INTERNAL_URL using CRON_SECRET; Compose configures the internal URL.

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 and worker images occasionally: docker image prune -f (or remove specific beenvoice:* / beenvoice-worker:* 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
EMAIL_PROVIDER, EMAIL_FROM Select mailpit, smtp, or resend and configure the sender
SMTP_HOST, SMTP_PORT, SMTP_SECURE Local Mailpit or another SMTP-compatible transport
RESEND_API_KEY, RESEND_DOMAIN, RESEND_FROM Production Resend delivery
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.