Extract privacy and terms content into reusable components, replace auth modals with links to /privacy and /terms, add settings legal section, and remove duplicate legal-modal markup. Co-authored-by: Cursor <cursoragent@cursor.com>
9.5 KiB
beenvoice server architecture
Dense reference for the Next.js web application and API in beenvoice/. Package manager: Bun. Database: PostgreSQL via Drizzle ORM.
Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16 App Router (src/app/) |
| API | tRPC 11 (/api/trpc), SuperJSON transformer |
| ORM | Drizzle + pg pool |
| Auth | better-auth (email/password, optional Authentik OIDC, Expo plugin for mobile) |
| UI | shadcn/ui, Tailwind CSS v4, Radix primitives |
| Resend | |
@react-pdf/renderer |
Request flow
Browser / Mobile / MCP client
│
├─► /api/auth/* → better-auth handler (session cookies)
├─► /api/trpc/* → createContext() → appRouter
│ ├─ Bearer / x-api-key → api-key auth
│ └─ else → better-auth session
├─► /api/mcp → API key only → JSON-RPC tools → tRPC caller
├─► /api/i/[token]/pdf → public invoice PDF
└─► /dashboard/* → RSC + client components (session required in UI)
Context (src/server/api/trpc.ts): protectedProcedure requires ctx.session.user. API-key auth sets authSource: "api-key"; apiKeys.* mutations require session (cannot manage keys with a key).
Directory layout
src/
├── app/ # Routes (pages + route handlers)
│ ├── api/
│ │ ├── auth/ # better-auth catch-all + custom register/reset REST
│ │ ├── trpc/[trpc]/ # tRPC HTTP adapter
│ │ ├── mcp/ # MCP over HTTP (API key)
│ │ ├── i/[token]/pdf/ # Public PDF
│ │ └── cron/ # Recurring invoice generation (CRON_SECRET)
│ ├── auth/ # sign-in, register, forgot/reset password
│ ├── dashboard/ # Authenticated app shell
│ └── i/[token]/ # Public invoice view
├── components/ # Shared UI (ui/, forms/, layout/, data/)
├── hooks/
├── lib/ # auth.ts, pdf-export, email templates, branding
├── server/
│ ├── api/
│ │ ├── root.ts # appRouter composition
│ │ ├── trpc.ts # procedures, context, timing middleware (dev)
│ │ ├── api-keys.ts
│ │ └── routers/ # one file per domain
│ └── db/
│ ├── schema.ts # all tables (prefix beenvoice_)
│ ├── index.ts # drizzle + pool
│ └── migrate.ts
├── trpc/ # react.tsx (client), server.ts (RSC)
├── env.js # @t3-oss/env-nextjs validation
└── styles/globals.css
drizzle/ # SQL migrations (0000–0014+)
tRPC routers
Root: src/server/api/root.ts. All routers use Zod input validation.
| Namespace | File | Key procedures |
|---|---|---|
clients |
routers/clients.ts |
getAll, getById, create, update, delete |
businesses |
routers/businesses.ts |
getAll, getById, getDefault, create, update, delete, setDefault, getEmailConfig, updateEmailConfig |
invoices |
routers/invoices.ts |
getAll, getBillable, getById, create, update, delete, updateStatus, bulk*, previewPdf, public token, getByPublicToken (public), sendReminder |
payments |
routers/payments.ts |
getByInvoice, create, delete |
expenses |
routers/expenses.ts |
getAll, getById, create, update, delete |
invoiceTemplates |
routers/invoiceTemplates.ts |
CRUD by template type |
recurringInvoices |
routers/recurring-invoices.ts |
CRUD, pause/resume, generateNow; cron helper generateDueRecurringInvoices |
timeEntries |
routers/time-entries.ts |
getAll, getRunning, clockIn, updateRunning, clockOut, create, update, delete, getSummary |
dashboard |
routers/dashboard.ts |
getStats |
email |
routers/email.ts |
sendInvoice |
settings |
routers/settings.ts |
profile, theme, animation prefs, export/import data, admin account roles |
apiKeys |
routers/apiKeys.ts |
list, create, revoke (session-only) |
Time clock semantics
- One running entry per user — partial unique index on
(createdById)whereendedAt IS NULL. clockIn— optional client, invoice, rate, backdatedstartedAt; resolves rate from input → client default → business default.clockOut— optional description update; computes hours; ifinvoiceIdset, appends line item; else tries latest open invoice for client.- Outcomes:
linked_to_invoice,saved_no_invoice,saved_no_client,zero_hours.
Database schema
Single file: src/server/db/schema.ts. Table names use pgTableCreator → prefix beenvoice_.
Auth & platform
| Table | Notes |
|---|---|
beenvoice_user |
Core user; role for admin features |
beenvoice_account |
OAuth/credential accounts (better-auth) |
beenvoice_session |
Sessions; unique token |
beenvoice_verification_token |
Email verification / reset |
beenvoice_api_key |
bv_ prefix keys; SHA-256 hash stored |
beenvoice_sso_provider |
OIDC/SAML config per user |
beenvoice_platform_setting |
Singleton (id = global) branding/PDF/appearance |
Domain
| Table | FKs | Notes |
|---|---|---|
beenvoice_client |
createdById → user |
defaultHourlyRate, currency |
beenvoice_business |
createdById |
Resend config, isDefault |
beenvoice_invoice |
client, business?, user | status draft/sent/paid; publicToken |
beenvoice_invoice_item |
invoice (cascade) | position ordering |
beenvoice_invoice_payment |
invoice, user | payment method enum |
beenvoice_expense |
business?, client?, invoice? | billable flags |
beenvoice_invoice_template |
user | notes/terms templates |
beenvoice_recurring_invoice |
client, business?, user | schedule, nextDueAt |
beenvoice_recurring_invoice_item |
recurring (cascade) | |
beenvoice_time_entry |
client?, invoice?, user | endedAt null = running |
Migrations: bun run db:generate → drizzle/; apply with db:push (dev) or db:migrate (prod script).
Authentication
Server — src/lib/auth.ts:
betterAuth+drizzleAdapter(users, sessions, accounts, verification)- Plugins:
@better-auth/expo(mobile SecureStore cookies),nextCookies(), optionalgenericOAuth(Authentik) - Email/password with bcrypt (12 rounds);
DISABLE_SIGNUPS=trueblocks registration trustedOrigins: production URL,beenvoice://,exp://(Expo)
Web client — src/lib/auth-client.ts: createAuthClient + genericOAuthClient.
Routes:
src/app/api/auth/[...all]/route.ts— better-auth handler- Custom REST:
register,forgot-password,reset-password,validate-reset-token(used by mobile and legacy flows)
Session cookies: better-auth.session_token or __Secure-better-auth.session_token in production.
Mobile API contract
The Expo app (beenvoice-app) does not use API keys. It:
- Calls the same tRPC endpoints with
Authorizationcookie header fromauthClient.getCookie(). - Stores session per account in SecureStore via
@better-auth/expo(storagePrefix:beenvoice:guestorbeenvoice:auth:{accountId}). - Requires
trustedOriginsand matchingBETTER_AUTH_URLfor the host the device can reach.
Ensure src/lib/auth.ts keeps the expo() plugin enabled.
MCP (machine clients)
POST /api/mcp — JSON-RPC 2.0, protocol 2025-11-25.
- Auth: API key only (
Authorization: Bearer bv_…orx-api-key). Session cookies rejected. - Tools: ~50 tools mirroring tRPC (invoices, clients, time clock, expenses, etc.)
- Implemented in
src/app/api/mcp/route.ts; delegates tocreateCaller(createContext).
API keys: format bv_<base64url>; stored as SHA-256 hash (src/server/api/api-keys.ts).
Environment variables
Validated in src/env.js. See .env.example.
| Variable | Required | Notes |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL connection string |
AUTH_SECRET |
prod | openssl rand -base64 32 |
BETTER_AUTH_URL |
yes | Public URL of API (no trailing path) |
NEXT_PUBLIC_APP_URL |
yes | Browser-facing URL |
DB_DISABLE_SSL |
local | true for Docker dev DB |
RESEND_API_KEY, RESEND_DOMAIN |
optional | Email; blank disables send |
AUTHENTIK_* |
optional | OIDC SSO |
DISABLE_SIGNUPS |
optional | true blocks registration |
CRON_SECRET |
cron route | Protects /api/cron/generate-recurring |
NEXT_PUBLIC_BRAND_* |
optional | Build-time white-label defaults |
Docker
| File | Use |
|---|---|
docker-compose.yml |
Production: app + db (Postgres internal) |
docker-compose.dev.yml |
Dev: Postgres only, port ${POSTGRES_PORT:-5432} |
App image built from Dockerfile; runs next start on port 3000.
Scripts
bun run dev # next dev --turbo
bun run build # production build
bun run db:push # push schema (dev)
bun run db:migrate # run migrations
bun run db:studio # Drizzle Studio
bun run check # eslint + tsc
Public / unauthenticated surfaces
invoices.getByPublicToken(tRPC publicProcedure)/i/[token]page and/api/i/[token]/pdf- Auth REST endpoints for register/reset
Related docs
- forms-guide.md, UI_UNIFORMITY_GUIDE.md
- data-table-responsive-guide.md
- email-features.md
- Mobile companion:
../beenvoice-app/docs/ARCHITECTURE.md