* docs: keep only server-specific contributor notes * docs: trim README to the code and link to beacon-docs * chore: strip tracking parameters from two airport website URLs
10 KiB
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.
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, andinternal/keystore/keystore_test.gofor examples of the style we use. - Integration tests (
*_integration_test.go) need a real PostgreSQL 16 and skip unlessBEACON_TEST_POSTGRES_DSNis 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:
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.
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.sqlis the 2.0.0 schema; do not modify existing migration files; append only via new files. - Update
db/queries/queries.sqlwith any new or modified queries - Re-run
sqlc generateto regeneratedb/sqlc/ - Update the store layer in
db/to expose the new functionality - Update
internal/api/reader.goif the change needs to be exposed via the API - Historical stats read hourly rollups, never raw observations; follow 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:
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// @Routercomments 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 initcommand from the checklist and commit the updateddocs/directory
Install swag if you don't have it:
go install github.com/swaggo/swag/cmd/swag@latest
Each handler function should have a godoc-style annotation block:
// 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:
// @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
CachedReaderininternal/cache/reader.go - Pass-through to
cr.innerif the data is highly dynamic, paginated with many filter combinations, or low traffic - Use
getOrSetwith an appropriate TTL category (cr.ttl.Stats,cr.ttl.Reference,cr.ttl.Nodes, orcr.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.govia theonNodeUpsertoronObserverUpsertcallbacks, 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 public dataset.
To refresh it with the latest airport data:
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):
AIRPORTS_CSV=/path/to/airports.csv go run ./internal/iatadb/gen
Adding a new dependency
- Run
go get <module>andgo mod tidy - Add an entry to 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. TheDBinterface iningest.godefines 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.
The server-specific step is bumping @version in cmd/beacon/main.go and regenerating Swagger:
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 in your PR.