![beenvoice Logo](public/beenvoice-logo.png) # 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](./docs/ARCHITECTURE.md) **Mobile companion:** [apps/mobile](../mobile/README.md) ## 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](https://bun.sh) 1.x - Docker & Docker Compose (for PostgreSQL locally or full-stack deploy) - Git ## Local development ### 1. Install the workspace ```bash cd beenvoice bun install cd apps/web ``` ### 2. Environment ```bash cp .env.example .env.local ``` Edit `.env.local` for local dev. Minimum: ```env 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. ```bash bun run --filter @beenvoice/web docker:up bun run email:preview ``` ### 3. Database Start Postgres (dev compose exposes port 5432): ```bash 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): ```bash 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: ```bash DEMO_ACCOUNT_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 ```bash bun run dev ``` Open [http://localhost:3000](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`](../../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 ```bash cp .env.example .env ``` Set at least: ```env AUTH_SECRET= 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`: ```bash 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) ```bash ../../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:`) 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 ```bash 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](https://coolify.io) deploys (especially `ENOTFOUND garage` with Application + separate Garage compose), see **[docs/COOLIFY.md](./docs/COOLIFY.md)**. Recommended: deploy the root [`docker-compose.coolify.yml`](../../docker-compose.coolify.yml) as a single Compose resource. ### 4. Sign-ups Registration is **enabled** by default. To block new email/password accounts: ```env 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](./docs/ARCHITECTURE.md) for routers, schema, auth flows, and MCP. ## Scripts ```bash # 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](#docker-deployment-app--database)), 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](./docs/ARCHITECTURE.md) | Stack, routers, schema, auth, Docker, MCP | | [docs/COOLIFY.md](./docs/COOLIFY.md) | Coolify deploy paths and Garage networking | | [docs/README.md](./docs/README.md) | Index of UI and product guides | | [AGENTS.md](./AGENTS.md) | Conventions for AI-assisted development | ## License MIT — see the root [LICENSE](../../LICENSE).