Files
beacon-server/CONTRIBUTING.md
MrAlders0n 1f83df8583 docs: move operator docs to beacon-docs (#206)
* 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
2026-10-02 21:43:03 -04:00

275 lines
10 KiB
Markdown

# 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 <module>` 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.