5.8 KiB
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/)
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/)
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)
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:
publicschema:tenants,invites,super_admins,platform_settingstenant_<uuid>schema: all workshop data (users, clients, vehicles, work_orders, etc.)
Every API request sets search_path = tenant_<uuid>, 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 <access_token>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_tokenin body (15 min) +refresh_tokenin 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()whenTEST_DATABASE_URL/TEST_REDIS_URLare absent — safe to run without infra. - Backend uses
testify/assertandtestify/require. - Frontend uses Vitest + Testing Library with jsdom.
- Test setup file:
frontend/src/test/setup.ts.