Contributing
Conventional commits
Commit messages follow conventional commits — Husky rejects anything else.
feat: add primary community change endpoint
fix: prevent duplicate upvotes on issues
chore: bump gin to 1.10
docs: describe representative dashboard flow
test: cover petition milestone thresholds
refactor: split issue handler into subhandlers
ci: add render build cache
Type reference:
feat:— new featurefix:— bug fixchore:— dependency bumps, toolingdocs:— documentation onlytest:— tests onlyrefactor:— no behaviour change, code shape onlyci:— pipeline / infraperf:— performance without behaviour change
Prefer one commit per logical change. If a PR ends up with 12 commits, squash on merge.
The 10 engineering rules
- Dependency injection always.
NewRepository(db)→NewService(repo)→NewHandler(svc)wired inmain.go. Never instantiate inside a struct. - No stringly-typed IDs. All entity IDs are UUIDs
(
uuid.New().String()). - Validate at boundaries. Use
binding:"required"on input structs. Never trust raw request data. - Single responsibility per file.
auth/service.godoes not touch HTTP. - Conventional commits — enforced by Husky.
- Files under 300 lines. Split at 500.
- Store timestamps in UTC. Localize only in the UI.
- Error codes, not raw messages. Every service returns
{code, message}. - Log for operators. Include service name, route, status. Never log passwords or tokens.
PasswordHashnever serialised. Usejson:"-"on any sensitive field.
Details in CLAUDE.md
at the repo root.
Adding a Go endpoint
Reach for the standard trio in the target service:
- Repository — write the GORM query in
internal/<feature>/repository.go. - Service — business logic in
internal/<feature>/service.go. Consumes the repository interface. - Handler — Gin handler in
internal/<feature>/handler.go. Binds input, calls the service, wraps the response. - Route — register in
handler.RegisterRoutes(rg)and mount fromcmd/server/main.go. - Gateway — add a
POST/GET/PATCH …line toservices/api-gateway/cmd/server/main.gowith the right rate-limit tier. - Docs — add the endpoint to
docs/api/openapi-<svc>.yamlwith summary, description, request/response examples, and status codes. Then re-sync to the embedded copy:cp docs/api/openapi-*.yaml services/api-gateway/internal/docs/openapi/
Adding a frontend feature
- State — TanStack Query for anything the server owns. Local
useStatefor anything ephemeral. Avoid Redux. - Forms — react-hook-form + zod resolver.
- Types — from
@civicos/typesif the value crosses the wire; from the feature folder if it's app-local. - i18n — every user-visible string in the citizen app goes into
apps/web/src/i18n/locales/en.json. The admin console is English-only for now.
Running tests
# Frontend
pnpm --filter @civicos/web test:e2e
pnpm --filter @civicos/admin test:e2e
# Go — from a service directory
go test ./...
E2E tests use Playwright. They spin up their own web server, so you
don't need pnpm dev running.
Before you push
pnpm typecheck # tsc --noEmit across every TS workspace
pnpm lint # eslint across every TS workspace
# from a Go service:
go vet ./...
go test ./...
Husky runs these on commit staged files. Don't skip hooks —
--no-verify is off-limits unless you've discussed with the maintainers.
Pull requests
- Small. Big PRs are hard to review — split.
- Titled with the type prefix matching your commit convention.
- Description explains the why. The diff already shows the what.
- Screenshots for UI changes. Before / after is ideal.
Where to ask
- Documentation — this Docusaurus site plus
CLAUDE.mdand the five PDFs indocs/product/. - Chat — TBD, ask the maintainers.
- Filing an issue — GitHub. Attach reproduction steps.