Add scheduled invoice delivery
This commit is contained in:
@@ -6,15 +6,15 @@ This application is the server and browser workspace in the Beenvoice monorepo.
|
||||
|
||||
## 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 |
|
||||
| Email | Resend |
|
||||
| PDF | `@react-pdf/renderer` |
|
||||
| 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 |
|
||||
| Email | Resend |
|
||||
| PDF | `@react-pdf/renderer` |
|
||||
|
||||
## Request flow
|
||||
|
||||
@@ -69,20 +69,20 @@ drizzle/ # SQL migrations (0000–0014+)
|
||||
|
||||
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; due generation runs through the worker |
|
||||
| `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) |
|
||||
| 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; due generation runs through the worker |
|
||||
| `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
|
||||
|
||||
@@ -97,30 +97,30 @@ Single file: `src/server/db/schema.ts`. Table names use `pgTableCreator` → pre
|
||||
|
||||
### 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 |
|
||||
| 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 |
|
||||
| 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).
|
||||
|
||||
@@ -166,27 +166,27 @@ API keys: format `bv_<base64url>`; stored as SHA-256 hash (`src/server/api/api-k
|
||||
|
||||
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; use string `true`/`false` (parsed in `src/env.js`) |
|
||||
| `CRON_SECRET` | cron route | Protects `/api/cron/generate-recurring` |
|
||||
| `NEXT_PUBLIC_BRAND_*` | optional | Build-time white-label defaults |
|
||||
| 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; use string `true`/`false` (parsed in `src/env.js`) |
|
||||
| `CRON_SECRET` | worker / cron routes | Protects worker delivery and `/api/cron/generate-recurring` |
|
||||
| `NEXT_PUBLIC_BRAND_*` | optional | Build-time white-label defaults |
|
||||
|
||||
## Docker
|
||||
|
||||
| File | Use |
|
||||
|------|-----|
|
||||
| Root `docker-compose.yml` | Deploy: `app` + `worker` + `db` + Garage; use `apps/web/.env` |
|
||||
| Root `docker-compose.dev.yml` | Local dev: Postgres + Garage |
|
||||
| File | Use |
|
||||
| ----------------------------- | ------------------------------------------------------------- |
|
||||
| Root `docker-compose.yml` | Deploy: `app` + `worker` + `db` + Garage; use `apps/web/.env` |
|
||||
| Root `docker-compose.dev.yml` | Local dev: Postgres + Garage |
|
||||
|
||||
The root `Dockerfile` has separate web and worker targets. The web container runs migrations and then `next start` on port 3000. The Bun worker uses PostgreSQL as its durable queue, polls with row locking, and schedules recurring invoice generation without Redis or an external cron. Docker builds run `next build` on Node 22 (not Bun) to avoid arm64 worker crashes; runtime stays on Bun.
|
||||
The root `Dockerfile` has separate web and worker targets. The web container runs migrations and then `next start` on port 3000. The Bun worker uses PostgreSQL as its durable queue, polls with row locking, generates recurring invoices, and triggers idempotent scheduled invoice delivery without Redis or an external cron. User-selected wall times are converted by the client to absolute instants and stored with their IANA timezone for consistent cross-device display. Docker builds run `next build` on Node 22 (not Bun) to avoid arm64 worker crashes; runtime stays on Bun.
|
||||
|
||||
Set `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` to the public hostname before deploy. Rebuild the image when changing `NEXT_PUBLIC_*` build-time vars.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user