# Contributing to Beacon Thank you for your interest in contributing. Beacon is a focused project and we want contributions to be high quality and sustainable. Please read this guide before opening a PR. --- ## Workflow Branches, the pull request flow, commit message format and the release process are the same for every Beacon repo and live in [beacon-docs/CONTRIBUTING.md](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/CONTRIBUTING.md). In short: open or comment on an issue first, branch from `dev`, keep each PR to one change, write conventional commit messages, and open the PR against `dev`. Running beacon-web against a local server is described there too. Before opening a PR here: ``` go build ./... # must compile gofmt -l . # must be empty (no unformatted files) go vet ./... # no warnings go test ./... # all tests pass govulncheck ./... # no reachable vulnerabilities swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency # if you changed any handler or api type (see below) ``` --- ## Code style - Follow the existing patterns in each package before introducing new ones - Keep functions small and single-purpose - Prefer explicit error handling over panic --- ## Tests - **Add tests for any new pure functions.** Pure functions (no DB, no network, no side effects) should have unit tests. See `internal/hub/hub_test.go`, `internal/api/nodes_test.go`, and `internal/keystore/keystore_test.go` for examples of the style we use. - Integration tests (`*_integration_test.go`) need a real PostgreSQL 16 and skip unless `BEACON_TEST_POSTGRES_DSN` is set. CI runs a selected set; run the ones touching your change locally: `BEACON_TEST_POSTGRES_DSN=postgres://... go test ./db/ -run Postgres` - If you are fixing a bug, add a test that would have caught it. --- ## Backup export tests CI runs export, restore round-trip, offline verify and truncated-archive rejection against PostgreSQL 16. Locally, with `PG*` pointing at a throwaway server whose role can create databases: ```sh go build -o "$PWD/beacon-backup" ./cmd/beacon-backup BEACON_BACKUP_TEST_POSTGRES=1 BEACON_BACKUP_TEST_BINARY="$PWD/beacon-backup" \ go test ./internal/backup -run '^TestExportPostgres$' -v ``` The test creates, migrates, restores and drops two randomly named databases. The export format and operator usage are documented in [Backup and export](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/docs/backup-export.md). --- ## Database changes All schema changes must include a proper migration path: - Add a new migration file to `db/migrations/` following the existing naming convention (e.g. `002_add_observation_count.sql`). `001_baseline.sql` is the 2.0.0 schema; do not modify existing migration files; append only via new files. - Update `db/queries/queries.sql` with any new or modified queries - Re-run `sqlc generate` to regenerate `db/sqlc/` - Update the store layer in `db/` to expose the new functionality - Update `internal/api/reader.go` if the change needs to be exposed via the API - Historical stats read hourly rollups, never raw observations; follow [docs/historical-stats.md](docs/historical-stats.md) to add a metric Never edit files under `db/sqlc/` by hand; they are generated by sqlc and will be overwritten. If you need to work around a sqlc limitation, document it clearly in the query comment. To regenerate after modifying `db/queries/queries.sql`: ```bash sqlc generate ``` --- ## API changes ### Router configuration and optional admin endpoints Keep `router.New`'s signature stable. Pass startup settings by name in `router.Options`; a new optional setting must not require unrelated callers and tests to supply another positional argument. An optional admin feature owns its subrouter, dependencies and HTTP methods. Register it once through `Options.AdminRoutes` in the startup wiring, using a literal path such as `/accounts`. The parent admin router applies bearer authentication to the entire subtree; the feature must not mount itself beside that boundary. `/config` remains owned by the shared admin router. No global registration or `init` side effects are needed. When concurrent PRs touch the same startup wiring, stack them in an explicit merge order, regenerate Swagger rather than hand-merging it, and build the combined result even when GitHub says it merges cleanly. Any new or modified REST endpoint must have swagger annotations and regenerated docs: - Add or update `// @Summary`, `// @Param`, `// @Success`, `// @Failure`, and `// @Router` comments on the handler function - Response types are defined in `internal/api/`; add new types there, not inline in handlers - After changing any handler or API type, rerun the `swag init` command from the checklist and commit the updated `docs/` directory Install swag if you don't have it: ```bash go install github.com/swaggo/swag/cmd/swag@latest ``` Each handler function should have a godoc-style annotation block: ```go // listThings godoc // // @Summary Short description shown in the UI // @Tags TagName // @Produce json // @Param paramName query string false "Description" // @Param id path string true "Resource ID" // @Success 200 {object} api.MyResponseType // @Failure 400 {object} handlers.APIError // @Failure 500 {object} handlers.APIError // @Router /things [get] func listThings(reader api.Reader) http.HandlerFunc { ``` For paginated responses use the generic page wrapper: ```go // @Success 200 {object} api.Page[api.MyType] ``` --- ## Caching Beacon uses a Redis-backed caching layer in `internal/cache`. If you add a new REST endpoint backed by a new `api.Reader` method, consider whether it should be cached: - Add the method to `CachedReader` in `internal/cache/reader.go` - Pass-through to `cr.inner` if the data is highly dynamic, paginated with many filter combinations, or low traffic - Use `getOrSet` with an appropriate TTL category (`cr.ttl.Stats`, `cr.ttl.Reference`, `cr.ttl.Nodes`, or `cr.ttl.Observers`) for read-heavy, slow-changing responses - If the data is mutated by the ingest path, add an invalidation call in `internal/ingest/side_effects.go` via the `onNodeUpsert` or `onObserverUpsert` callbacks, or add a new callback following the same pattern Cache keys live as constants at the top of `reader.go`. Use the `beacon:` namespace prefix and include all parameters that affect the response in the key. --- ## Updating the IATA database Beacon includes a static IATA → country/continent mapping compiled into the binary, generated from the [OurAirports](https://ourairports.com/data/) public dataset. To refresh it with the latest airport data: ```bash rm internal/iatadb/gen/airports.csv go generate ./internal/iatadb/ ``` This fetches a fresh `airports.csv` from OurAirports, saves it locally, and regenerates `internal/iatadb/db.go`. Commit both files. To use a local CSV instead (e.g. in a restricted network environment): ```bash AIRPORTS_CSV=/path/to/airports.csv go run ./internal/iatadb/gen ``` --- ## Adding a new dependency - Run `go get ` and `go mod tidy` - Add an entry to [SHOULDERS.md](SHOULDERS.md) with a brief description of what the dependency does and why it was added --- ## Project structure ``` cmd/beacon/ main entry point, wiring, startup cmd/beacon-backup/ standalone backup export/verify command db/ store layer: sqlc-generated code + thin mapping layer migrations/ SQL schema (2.0.0 baseline + numbered files, append only) queries/ SQL queries (input to sqlc) sqlc/ generated Go code (do not edit by hand) internal/ api/ response types and Reader interface handlers/ HTTP handlers (validation, routing, response) middleware/ auth, rate limiting, logging router/ chi router wiring background/ periodic maintenance (retention, rollups, age-out) backup/ database + config bundle export and verification borders/ foreign-repeater classification against IATA borders cache/ Redis caching layer over the Reader config/ config loading and scope key derivation hub/ WebSocket fan-out broker iatadb/ in-memory IATA airport lookup ingest/ MQTT packet ingestion and side effects keystore/ channel key lookup logging/ slog setup lora/ LoRa airtime calculation meshmapper/ MeshMapper scope and zone imports presence/ coalesced last-seen writes profiling/ opt-in CPU profiling scopestore/ transport scope key lookup ws/ WebSocket connection handling docs/ generated swagger and design notes (swagger: do not edit by hand) ``` Key patterns to understand before contributing: - **Store layer** (`db/`): thin wrappers around sqlc-generated queries. Each method maps between the ingest/api param structs and sqlc param structs. Never put business logic here. - **Ingest layer** (`internal/ingest/`): processes raw MQTT packets, calls the store, and broadcasts hub events. The `DB` interface in `ingest.go` defines exactly what the ingest layer needs from the store; keep it minimal. - **Hub** (`internal/hub/`): pure fan-out broker. Events are pre-serialised JSON before entering the hub so the broadcast loop never touches encoding. - **Reader interface** (`internal/api/reader.go`): defines everything the API layer can read. The store implements it. All handler tests use a stub reader. --- ## Releases The flow is in [Releases and versioning](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/docs/releases.md#cutting-a-release). The server-specific step is bumping `@version` in `cmd/beacon/main.go` and regenerating Swagger: ```bash swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency ``` --- ## Recognition If you'd like to be listed as a contributor, add yourself to [CONTRIBUTORS.md](CONTRIBUTORS.md) in your PR.