# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this project is **TechXCar** — a multi-tenant SaaS for car workshop management. Each workshop gets an isolated PostgreSQL schema. A super-admin manages the platform and provisions workshops via invite links or manual creation. Design spec: `docs/superpowers/specs/2026-06-16-techxcar-design.md` --- ## Commands ### Backend (run from `backend/`) ```bash go run ./cmd/server # start dev server (requires .env) go build ./... # compile check go test ./... # all tests (integration tests skip without env vars) go test ./internal/server/... # single package go test -run TestLoad_defaults ./internal/config/... # single test ``` Integration tests require: - `TEST_DATABASE_URL=postgres://...` - `TEST_REDIS_URL=redis://...` ### Frontend (run from `frontend/`) ```bash npm run dev # localhost:5173 (proxies /api → localhost:8080) npm run build # TypeScript check + Vite build npm run test:run # Vitest single run npm test # Vitest watch npm run test:coverage ``` ### Docker (run from root) ```bash cp .env.example .env # first time only docker compose up # dev: all 4 services docker compose up backend # backend only (postgres + redis still needed) docker compose -f docker-compose.prod.yml up # production stack ``` --- ## Required env vars | Var | Notes | |---|---| | `DATABASE_URL` | postgres connection string — required | | `JWT_SECRET` | min 32 chars — required | | `REDIS_URL` | defaults to `redis://localhost:6379` | | `PORT` | defaults to `8080` | | `APP_ENV` | defaults to `development` | SMTP, Telegram, PDF storage, and billing data are **not** in `.env` — they live in `platform_settings` / `tenant_settings` tables, cached in Redis for 5 minutes, and managed through the Web UI settings panel. --- ## Architecture ### Multi-tenancy (critical to understand) PostgreSQL uses **schema-per-tenant**: - `public` schema: `tenants`, `invites`, `super_admins`, `platform_settings` - `tenant_` schema: all workshop data (users, clients, vehicles, work_orders, etc.) Every API request sets `search_path = tenant_, public` via `db.SetTenantSchema()` using the `tenant_id` claim from the JWT. The `tenantID` is validated against `^[a-zA-Z0-9_-]{1,63}$` before being interpolated into SQL (see `pkg/database/database.go`). When a new tenant is provisioned, `db.ProvisionTenantSchema()` creates the schema and runs `migrations/tenant/` against it. ### Backend layout ``` backend/ ├── cmd/server/main.go # entrypoint: loads config → DB → Redis → migrate → serve ├── internal/ │ ├── config/ # typed config from env (DATABASE_URL, JWT_SECRET required) │ └── server/ # Fiber app, security middleware, route registration ├── pkg/ │ ├── database/ # pgxpool wrapper + MigratePublic + ProvisionTenantSchema │ └── redis/ # go-redis wrapper └── migrations/ ├── public/ # applied at startup via MigratePublic() └── tenant/ # applied per-tenant via ProvisionTenantSchema() ``` Future internal packages (per spec): `auth/`, `tenant/`, `workorder/`, `client/`, `catalog/`, `invoice/`, `staff/`, `expense/`, `notification/`, `report/`, `settings/`. ### API conventions - Base path: `/api/v1/` - Auth: `Authorization: Bearer ` header - Response envelope: `{ "data": ..., "error": null, "meta": { "page": ..., "total": ... } }` - Errors via the Fiber error handler in `internal/server/server.go` ### JWT flow - `POST /api/v1/auth/login` → `access_token` in body (15 min) + `refresh_token` in httpOnly cookie (30 days) - `POST /api/v1/auth/refresh` → new access token using the cookie - Rate limiting on auth routes: 10 attempts/min per IP (Redis) ### Frontend layout (planned — frontend/ not yet scaffolded as of Plan 1) ``` frontend/src/ ├── pages/ │ ├── auth/ # /login │ ├── admin/ # /admin/* — super_admin only │ └── app/ # /app/* — tenant roles ├── components/ │ ├── ui/ # shadcn/ui base components │ └── shared/ # reusable app components ├── hooks/ ├── lib/ │ ├── api.ts # fetch wrapper with auto token refresh on 401 │ └── queryClient.ts # TanStack Query config ├── store/ # Zustand stores (authStore persisted to localStorage) └── i18n/ # pt-PT default; en structure prepared but not translated ``` Routes: `BrowserRouter`. Role guards in `App.tsx` redirect to `/login` for unauthorized roles. ### Async notifications Telegram and email notifications are **not sent inline**. The HTTP handler publishes a job to a Redis list; a dedicated goroutine in `internal/notification/` consumes the queue and sends. This means HTTP responses are not blocked by notification delivery. ### Work order state machine ``` open → in_progress → completed → invoiced ↓ cancelled (from any state except invoiced) ``` State transitions are logged to `wo_status_log` with timestamp and the user who triggered the change. ### PDF generation Server-side in Go using `chromedp` (HTML template → Chrome headless → PDF). PDFs stored on a Docker volume (`/app/storage`) or S3-compatible storage (configurable in settings). Served via an authenticated endpoint, not from the webroot. --- ## Testing conventions - Integration tests use `t.Skip()` when `TEST_DATABASE_URL` / `TEST_REDIS_URL` are absent — safe to run without infra. - Backend uses `testify/assert` and `testify/require`. - Frontend uses Vitest + Testing Library with jsdom. - Test setup file: `frontend/src/test/setup.ts`.