Coding Standards

Service scaffold · commit conventions · dispatch protocol.
Source: CanaryGo/docs/conventions.md — if this page disagrees with that file, the file wins.

Service Scaffold

Every domain module lives under internal/<module>/. Standard layout:

internal/<module>/
  handler.go— HTTP layer: parse → delegate → render. No business logic.
  handler_test.go— Table-driven unit tests for every route.
  store.go— pgxpool-backed DB access. Concrete *Store + interfaces for DI.
  dto.go— Wire-shape types: request/response structs with JSON tags.
  types.go— Domain types when DTO ↔ entity shapes diverge.
  integration_test.go— //go:build integration — runs against real Docker postgres.
cmd/<service>/
  main.go— Binary entry point: config, pool, router wiring, http.ListenAndServe.

Mount pattern

Every domain package exposes a single Mount(r chi.Router) that registers its routes. The binary's main.go calls each module's Mount in sequence. No route registration outside of the module that owns it.

Handler conventions

ConcernConvention
Path style/v1/<resource>[/{id}] — lower-snake-case segments. Most-specific routes first.
Pagination?page=N&size=M — 1-indexed, 50 default / 200 max.
Success envelope{"items":[…],"page":1,"size":50,"count":17} for collections. Single resource returned directly.
Error envelope{"code":"snake_case_error","message":"detail"} — code is the stable wire identifier; message is advisory.
Body limithttp.MaxBytesReader(w, r.Body, 1<<20) — 1 MiB unless the handler documents otherwise.

Error mapping

SentinelHTTP statusCode
ErrNotFound404not_found
ErrConflict409conflict
ErrValidation400invalid_request
(other)500internal_error — message redacted, error logged via zap

Store interface pattern

The store exposes a concrete *Store (constructed via NewStore(pool)) and one or more narrow interfaces (Reader, Writer, or method-named). Handlers accept the interface — tests substitute a stub. Sentinel errors are package-scoped; no shared errs package.

sqlc rule

Read paths: direct pgx + inline SQL is acceptable.
Simple writes (single INSERT/UPDATE/DELETE): direct pgx is acceptable.
Complex writes (multi-statement tx, cross-table, ≥3 callers): use sqlc.
Queries go in internal/db/sqlc/<module>.sql. Generated output in internal/db/query/<module>/ — do not hand-edit. Regenerate with make sqlc-gen.

Decimal standard

All money fields use github.com/shopspring/decimal against numeric(14,4) columns. Wire format is string ("12.34"). Currency travels as a separate string field — no Money wrapper struct.

Commit Conventions

Commits follow a structured format so the log is machine-readable and dispatch-traceable.

<type>(<scope>): <description> — <ticket>

Optional body: what changed and why. Bullet list preferred for multi-point changes.

Co-Authored-By: Claude Sonnet 4.x <noreply@anthropic.com>
TypeWhen to use
featNew capability — a route, a store method, a migration that adds surface.
fixBug fix. Scope is the affected module.
demoWork scoped to the GRO-802 demo workstream (Square OAuth, vault, discovery).
choreNon-functional: dependency updates, gitignore changes, file moves.
testTests only — no production code change.
docsDocumentation only.
refactorCode change with no behavior change — naming, extraction, restructure.

One dispatch per commit series. A commit that touches three modules to deliver one ticket is correct. Three commits across three dispatches that all land in the same PR is a process error.

Dispatch Protocol

All work in this repo is Linear-dispatch-driven. There are no free-form commits to main without a corresponding ticket. The dispatch is the unit of accountability — it carries the brief, the target, the priority, and the completion record.

Lifecycle

StepAction
1 — CreateOpen a Linear issue in the Dispatch project. Imperative title (add location filter to inventory handler). Description carries the full brief — expected inputs, expected outputs, acceptance criteria.
2 — Pick upSet status → In Progress. Post a comment confirming pickup and estimated completion.
3 — ExecuteCode, test, commit per the conventions above. Outputs land in the repo at the paths named in the dispatch.
4 — CompleteSet status → Done. Post a closing comment: artifact paths, commit SHA, one-paragraph summary, any follow-on tickets recommended.
FailureSet status → Cancelled with a reason comment. Do not leave issues In Progress indefinitely.

Targets and agents

Issues carry Target/laptop or Target/any labels and optionally an Agent/<name> label. Priority is Linear's native field. Do not duplicate priority in labels.

What stays in Linear vs the repo

Linear — dispatch status, pickup confirmation, completion record, mid-flight conversation.
Repo — all artifacts a dispatch produces: specs, plans, SDDs, Brain wiki cards, code.
Linear references repo paths. The repo does not track Linear status. These are two separate planes.

First dispatch for new contributors

The first dispatch for any new contributor is: walk the onboarding path and propose patches via PR for any confusing surfaces or gaps. This is the right first task — it exercises the full dispatch lifecycle while producing genuine value (fixing the thing that was confusing is better than any contrived starter task).