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.
Open or comment on an issue before starting work. This avoids duplicate effort and lets maintainers flag if something is already in progress or out of scope. For small bug fixes a brief comment is fine; for larger features please discuss the approach first.
One thing per PR. Each pull request should cover one logical change — a bug fix, a new endpoint, a refactor, a new test. PRs that touch many unrelated parts of the codebase are hard to review and hard to revert if something goes wrong.
No fully AI-generated contributions. We welcome developers who use AI tools to assist their work, but PRs should reflect the author's own understanding and judgement. PRs that appear to be unreviewed AI output may be closed without further comment.
main— stable releases only, protected. Never target this directly.dev— active development. All PRs targetdev.
- Fork or create a branch from
dev - Make your changes
- Run the checklist below
- Open a pull request against
devwith a clear description of what changed and why, referencing any related issues
go build ./... # must compile
gofmt -l . # must be empty (no unformatted files)
go vet ./... # no warnings
go test ./... # all tests pass
swag init # if you changed any handler or api type (see below)
- Run
gofmt -w .before committing — CI will fail on unformatted files - Run
go vet ./...— no warnings - Follow the existing patterns in each package before introducing new ones
- Keep functions small and single-purpose
- Prefer explicit error handling over panic
- 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 (requiring a real DB) are not yet required but are welcome. They will be gated before release.
- Run
go test ./...before opening a PR. All tests must pass. - If you are fixing a bug, add a test that would have caught it.
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). 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
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 generateAny 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, regenerate the swagger docs and commit
the updated
docs/directory alongside your changes:
swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependencyInstall swag if you don't have it:
go install github.com/swaggo/swag/cmd/swag@latestEach 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]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.
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- 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
Use the conventional commits format:
feat(routes): add observation_count to known routes response
fix(ingest): correct lat/lon divisor for advert payloads
refactor(hub): collapse RegionIATAs into IATAs in Scope
test(api): add unit tests for NodeTypeName and NodeTypeFromString
chore: update airports.csv
docs: expand CONTRIBUTING.md
Scopes are optional but helpful for larger codebases. Common scopes: api,
db, ingest, hub, ws, handlers, config, keystore.
cmd/beacon/ — main entry point, wiring, startup
db/ — store layer: sqlc-generated code + thin mapping layer
migrations/ — SQL schema (single file, 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)
router/ — chi router wiring
config/ — config loading and scope key derivation
hub/ — WebSocket fan-out broker
ingest/ — MQTT packet ingestion and side effects
iatadb/ — in-memory IATA airport lookup
keystore/ — channel key lookup
scopestore/ — transport scope key lookup
ws/ — WebSocket connection handling
docs/ — generated swagger docs (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.
Merges from dev to main are done by maintainers and represent a versioned
release. Do not open PRs directly against main.
NOTE: release are done manually, not from GH. Commits for releases should be signed.
- Ensure all changes are committed and CI is green on
dev - Bump the version string in the
@versionswagger annotation incmd/beacon/main.go - Regenerate swagger docs:
swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency- Commit the version bump and updated docs:
chore: bump version to vX.Y.Z
- Merge
devintomain(fast-forward only):
git checkout main
git merge --ff-only dev- Tag the release and push:
git tag vX.Y.Z
git push origin main --tagsPushing the tag triggers the release CI workflow, which builds binaries for all supported platforms and attaches them to a draft GitHub release.
-
Open the draft release on GitHub, paste the release notes, and publish.
-
Rebase
devonmainto keep histories in sync for future releases:
git checkout dev
git rebase mainIf you'd like to be listed as a contributor, add yourself to CONTRIBUTORS.md in your PR.