# Torqvoice, for coding agents Torqvoice is workshop management for automotive (and marine) service businesses: customers, vehicles, work orders, quotes, invoices, inventory, inspections, messaging, bookkeeping sync. One codebase runs both the self-hosted install and Torqvoice Cloud, where each workshop is an organization. Next.js 16 App Router, TypeScript, PostgreSQL, Prisma 7, Better Auth, next-intl, Tailwind 4 with shadcn/ui, Biome, Vitest, Playwright. Licensed under the Elastic License 2.0. User docs live at https://torqvoice.com/docs and are written by the maintainer, not in this repo. `CONTRIBUTING.md` has setup, the CLA and the pull request rules; this file is what you need while writing code and cannot see from the code itself. ## Commands Open the repo in the Dev Container (`.devcontainer/`) and everything below works as is: Node 22, Postgres 16 as the `db` service on 5432, pgAdmin on 8080, Biome formatting on save. Outside it you need Node 22 and your own Postgres (`docker-compose up -d db`), then `cp .env.example .env`. ```bash npm run dev # http://localhost:3000, needs DATABASE_URL in .env npm run check # Biome lint + format check, what CI runs (check:fix writes) npm test # Vitest, jsdom, Prisma mocked npx vitest run src/__tests__/.test.tsx # one test file NODE_OPTIONS=--max-old-space-size=6144 npx tsc --noEmit # typecheck; without the flag it runs out of memory npm run test:db # the few tests that need Postgres; E2E_DATABASE_URL must contain "test" or "e2e" npm run test:e2e # Playwright against a built app and a real database, see e2e/README.md npx prisma generate # after a fresh clone or a schema change; output is src/generated (gitignored) npx prisma migrate dev --name # schema change; commit the folder it creates ``` Formatting is Biome only. There is no ESLint or Prettier config; running Prettier rewrites whole files. Schema changes go through `prisma migrate dev` and a reviewed migration folder, never `prisma migrate deploy` or `prisma db push`. ## Gotchas - `npm test` can end with every test passing and exit code 1: `EnvironmentTeardownError: Closing rpc while "onUserConsoleLog" was pending`. That is a Vitest worker race, not your change; rerun it. - A git worktree of this repo needs `node_modules` and `src/generated` symlinked from the main checkout before Vitest or `tsc` will run there. - `.env.example` sets `DATABASE_URL` to host `db`, which resolves inside the Dev Container. Outside it, change the host to `localhost`. ## Where things are | Path | What | | --- | --- | | `src/app/(authenticated)` | Signed-in pages. Server components fetch with Prisma, then hand data to a client component. | | `src/app/(public)`, `(designer)`, `(presenter)` | Auth, portal and share links; the invoice and email designers; the work board for a wall screen. | | `src/app/api` | Route handlers: webhooks, PDFs, uploads, the technician app API under `v1/tech`, WebSockets. | | `src/features//` | `Actions/` server actions, `Components/` client components, `Schema/` zod, `Lib/` helpers. | | `src/components/ui` | shadcn primitives. Shared app components sit one level up. | | `src/lib` | Auth, database, permissions, money, dates, realtime, backup, cron. | | `src/integrations//` | One connector per folder: `manifest.ts` + `server.ts`, listed in `registry.ts`. A test fails if a folder is missing from the registry. | | `prisma/schema/*.prisma` | The schema, split by area. `prisma/migrations/` is history; never edit an applied one. | | `messages//.json` | Translations. English is the source; other locales may lag. | | `src/__tests__/` | Vitest, mirroring `src/`. `e2e/specs/` is Playwright. | ### The app directory Every `page.tsx` and `layout.tsx` in `src/app` is a server component: it reads the session, fetches with Prisma or a feature's action, and renders. A page that needs interactivity hands its data to a client component. One that belongs only to that page may sit beside it as `-client.tsx`; anything reused lives in `src/features`. Route groups set the shell: ``` src/app/ ├── (authenticated)/ # signed-in workshop, layout.tsx has the sidebar and session │ ├── page.tsx # dashboard │ ├── vehicles/ # list, [id], [id]/service/new and [serviceId]; a work order is a service record │ ├── sales/ # counter sales: a work order without a vehicle │ ├── settings/ # one folder per settings page, settings-nav.tsx lists them │ └── admin/ # platform admin, super admins only ├── (public)/ # nobody signed in, every page is token or code gated │ ├── auth/ # sign-in, sign-up, 2FA, email verification, password reset │ ├── onboarding/ app-setup/ │ ├── portal/[orgId]/ # the customer portal │ ├── share/ # invoice, quote, inspection, status-report, terms by link │ ├── p/[token] # a phone opened from a work order's "Add from phone" code │ └── b/[token] # an online booking ├── (designer)/ # invoice-designer and email-designer, full-screen canvas ├── (presenter)/work-board/ # the work board for a wall screen ├── api/ │ ├── protected/ # route handlers for the signed-in app: uploads, files, ws, AI, backup │ ├── public/ # share links, portal, tracking pixels, images │ ├── webhooks/ # stripe, paypal, vipps, email, sms, whatsapp, telegram │ ├── integrations/[connector]/ # OAuth callbacks for connectors │ ├── v1/ # tech (the technician app), cron, health │ └── system/ # backup-heartbeat ├── layout.tsx # root: providers, theme, locale └── globals.css # theme tokens for light, dark and the colour themes ``` API routes are for what cannot be a server action: a browser upload, a webhook, a PDF, a WebSocket, the technician app. Everything else goes through an action. ## How code is written here **Server actions** are the only write path. Each lives in its own file under `src/features//Actions/`, is wrapped with `withAuth` from `src/lib/with-auth.ts`, and returns `ActionResult` (`{ success, data?, error?, gated?, forbidden? }`). `withAuth` hands you `ctx.organizationId`; every Prisma query is scoped by it, including lookups by id, because ids from the browser can belong to another workshop. Pass `requiredPermissions` when the action is not for everyone; `src/lib/permissions.ts` lists them. Validate input with zod before touching the database. **Four things an action may also need.** An action that changes a document, money or access passes `audit` to `withAuth` (`src/lib/audit.ts` has the event shape; 75 actions do this). One that sends a message or email or calls a paid service starts with `demoGuard()` from `src/lib/demo.ts`, because the public demo runs with real provider credentials; a test lists every unguarded one and fails. One a cloud plan may refuse returns `gated`, and the screen passes the result to `handleGated` from `src/components/upgrade-gate` before showing an error. And wording that names the vehicle has two forms: `useServiceType()` from `src/components/service-type-context` says whether this workshop fixes cars or boats, and the strings for both live side by side (`title` and `titleMarine`). **Action names are an API.** A tab opened before a deploy calls the new build with what the old page sends. Changing what an action takes, or what its input means, means a new action name; the old tab is then asked to reload instead of writing bad data. **Reads** happen in server components with `db` from `src/lib/db.ts`. A client component that must fetch uses TanStack Query (`src/lib/query-provider.tsx`) and parses the answer with zod. **Every string the user sees goes through next-intl.** Add the English under `messages/en/` in the namespace that owns the screen and read it with `useTranslations`. Playwright specs and the docs find buttons by their English words, so changing copy is a visible change, not a cleanup. **Dates and money.** The database holds UTC. Render in the workshop's time zone through `src/lib/timezone.ts` and `src/lib/workshop-datetime.ts`; no `setHours` or `getDay` on a raw `Date` on the server. Amounts carry a currency code (`src/lib/money.ts`), countries are ISO 3166-1, languages ISO 639-1. **Nothing country-specific by default.** Registries, tax rules, payment providers and parts sources ship as connectors or settings a workshop turns on, never as behaviour everyone sees. **UI.** Use the shadcn components and the theme tokens (`bg-background`, `text-muted-foreground`, `border`): the app has dark mode and six colour themes defined in `src/app/globals.css`, so a hard-coded colour is wrong in most of them. A dialog with a long form uses `DialogHeader`, `DialogBody` and `DialogFooter` from `src/components/ui/dialog`, so the buttons stay pinned while the fields scroll. Client components are `"use client"` files in `src/features`; pages in `src/app` stay server components. **Naming.** `src/components` and `src/lib` are kebab-case files. Inside `src/features` the folders are `Actions`, `Components`, `Schema`, `Lib` and component files are PascalCase. Imports use `@/`. ## Tests Vitest tests assert what a user would notice, not implementation details, and mock Prisma; `src/__tests__/setup.ts` wires next-intl. Anything that only breaks with the pieces assembled (migrations, cookies, forms wired to actions, invoice numbering) is a Playwright spec. CI runs `npm run check`, `npm test` and the e2e suite in eight shards on every pull request. ## Pull requests One change per PR, imperative title (it becomes the release note), body from `.github/PULL_REQUEST_TEMPLATE.md`: Summary, Why, Testing. First-time contributors keep the CLA line; the maintainer deletes it. Screenshots for UI changes. Before opening: `npm run check && npm test`.