cmd/api/holds the API server (hub-api): HTTP API, ingestion, record retrieval, tenant/auth; enqueues jobs to River (insert-only). Build/run:go run ./cmd/apiormake run.cmd/worker/holds the worker (hub-worker): runs River job workers (webhook delivery, embeddings). No HTTP. Build/run:go run ./cmd/workerormake run-worker.internal/contains core application layers:api/handlers,api/middleware,service,repository,models,config, andworkers.pkg/provides shared utilities (currentlypkg/database).migrations/stores SQL migration files (goose); use-- +goose up/-- +goose downannotations.tests/contains integration tests.
make dev-setup: start Postgres via Docker, install Go deps/tools, and initialize database schema.make run: run hub-api (config from.envif present and environment variables; copy.env.exampleto.envor set env vars).make run-worker: run hub-worker (requires DATABASE_URL from.envor environment variables).make build: build bothbin/hub-apiandbin/hub-worker. Usemake build-apiormake build-workerfor a single binary.make tests: run integration tests intests/.make tests-coverage: generatecoverage.html.make check-coverage: run all tests with coverage and fail if below COVERAGE_THRESHOLD (excludes cmd/api and cmd/worker main packages).make init-db: run goose migrations up usingDATABASE_URL.make migrate-statusandmake migrate-validatefor status and validation. New migrations go inmigrations/with goose annotations (-- +goose up/-- +goose down). Name files with a sequential number and short description (e.g.002_add_webhooks_table.sql); goose orders by the numeric prefix. For webhook delivery, runmake river-migrateafterinit-dbto apply River job queue migrations.make fmt: format code (runsgolangci-lint run --fix; uses gofumpt/gci from config).make lint: rungolangci-lint(includes format checks; requiresmake install-tools).
- Language: Go; format with
make fmt(golangci-lint applies gofumpt/gci). - Prefer Go naming conventions (CamelCase for exported, lowerCamel for unexported).
- Keep package names short and domain-focused (e.g.,
repository,service).
- Treat
tenant_idas a security boundary, not a convenience filter. Tenant-owned data must never be read, enqueued, dispatched, cached, searched, embedded, or deleted across tenants. - Exception: GDPR/right-to-erasure flows may intentionally delete all records for a data subject across tenants when that is the documented API contract. Make that all-tenant behavior explicit in the API docs, service/repository names or comments, logs, and tests; do not reuse it for normal tenant-owned workflows.
- When making a model, migration, API request, or repository change involving tenant-owned data, audit every downstream path that carries or derives from that data: handlers, services, repositories, message publishers, River job args, workers, webhook payloads, search, embeddings, bulk operations, logs, and metrics.
- Tenant access rules must be consistent across every path that can observe, mutate, derive from, or act on the same resource. If one API endpoint, repository method, search path, webhook dispatch path, worker, backfill, bulk operation, or export path requires tenant scope, every alternate path for that resource must enforce the same tenant boundary.
- Do not model
tenant_idas an optional filter for tenant-owned resources. Prefer required tenant parameters in service/repository method signatures (tenantID string, not*string) unless the domain explicitly supports global resources and documents that behavior. - Prefer tenant-aware repository/service methods for tenant-owned workflows. Avoid adding broad helpers that return all enabled/all matching resources when the caller is dispatching, processing, deriving, exporting, or exposing tenant data.
- Async jobs must carry the tenant boundary when the source data has one, and workers must re-check tenant scope before doing side effects. Do not rely only on enqueue-time filtering.
- Global resources may intentionally have
tenant_id = NULLonly when the domain explicitly documents them as non-tenant-owned. Webhooks are tenant-owned and must require a non-emptytenant_id; a missing tenant on event data must not match any webhook. - When changing access rules for a tenant-owned model, search for all alternate access paths by resource name and by derived side effects: list, get, update, delete, bulk delete, webhook fan-out, River jobs, workers, embeddings, search, exports, cache invalidation, logs, and metrics.
- For any tenant-scoping change, include verification at the boundary where the leak could happen: database query behavior, service fan-out, worker execution, and API behavior when relevant. Include at least one alternate-path regression test proving that data allowed through the primary path cannot leak through async dispatch, bulk operations, derived indexes, exports, or background workers. Tests are the evidence; the invariant belongs in the architecture.
- Tests live under
tests/and are run withgo test ./tests/.... - Name test files
*_test.goand test functionsTestXxx. - Use
make tests-coveragewhen adding meaningful logic to track coverage output.
- Always create a pull request for changes and don't commit directly to main.
- Commit messages follow a Conventional Commits style (e.g.,
chore: fix linting errors). - PRs should describe the change, include test evidence (command + result), and link related issues when available.
- For API behavior changes, include request/response examples or screenshots of Swagger UI.
- Configure
API_KEYandDATABASE_URLvia.envor environment variables. - Do not commit
.envor secrets; use.env.exampleas the base.