# Beacon configuration file # Copy to config.yaml and adjust as needed. # Durations are Go strings (30s, 1h, 168h); negative values prevent startup # (observers.delete_after excepted, where nonpositive disables it). # Public reads stay available. No key means admin routes return 503. # Prefer BEACON_API_KEY in the service environment; never commit a real key. # At least 16 characters with no inner whitespace (surrounding whitespace is # trimmed); a shorter key prevents startup. auth: api_key: "" log: # Environment LOG_LEVEL / LOG_FORMAT override these values. level: info # debug, info, warn, error format: text # text or json server: # Only these direct proxy peers may set the client IP through X-Real-IP. # The proxy must overwrite that header, not pass through client input. # Leave empty for direct access (logs a startup warning). Invalid/empty CIDR # entries prevent startup. A single host needs /32 for IPv4 or /128 for IPv6; # bare IP addresses are not accepted. # For Docker, list the actual proxy/compose network CIDR (e.g. 172.20.0.0/24). # For a local host proxy, use ["127.0.0.1/32", "::1/128"]. # Direct-client proxy examples; X-Forwarded-For alone is not used: # Apache 2.4.10+ (mod_headers, in the proxy virtual host): # RequestHeader set X-Real-IP "expr=%{REMOTE_ADDR}" # nginx (in the proxy location): # proxy_set_header X-Real-IP $remote_addr; # Caddy (inside the reverse_proxy block): # header_up X-Real-IP {http.request.remote.host} # With additional proxy hops, first configure their trust at the edge proxy # and pass its validated client address rather than a client-supplied header. trusted_proxies: [] # Address of the admin listener serving Prometheus /metrics, e.g. 127.0.0.1:9090 # (0.0.0.0:9090 inside Docker, published on the host's 127.0.0.1 only). Off when empty. # It has no authentication: bind it to a private interface. admin_listen: "" # read_only: true boots a server that never writes: no migrations, no ingest, no background jobs, no # MeshMapper imports, and every database session is read-only. Used to compare two builds on one database. read_only: false # Applies to /api/v1/* only. Both counters are local to this Beacon instance. # Set trusted_proxies correctly before using a reverse proxy, or clients share # the proxy's budget. IPv6 clients share a /64 budget to prevent address rotation. ratelimit: enabled: true requests_per_minute: 600 # Maximum requests in a one-second sliding window, not token-bucket capacity. # Omitted/zero defaults to requests_per_minute. Negative limits prevent startup. # A 429 returns error.code=rate_limited and Retry-After (1 or 60 seconds). burst: 600 # Database pools. custom_all (default) opens api_legacy 8, api_custom 8, ingest 6, maint 4 connections # (26); split opens 30 (api_custom 7, api_cached 5); single opens 18 (api_legacy, ingest, maint). # ingest grows to one connection per CPU on hosts with more than 6 CPUs, adding to each total. # Keep max_connections well above that. A pool_max_conns in POSTGRES_DSN is overridden. #database: # # custom_all (default): every API statement gets a custom plan. # # split: statements admitted to the generic-plan class (GetNodeByID, GetObserverTelemetrySince, # # ListKnownRoutesByIATA, ListKnownRoutesGlobal, ListObserverAdverts, ListPacketsByTypes, # # ListPacketsPage) run on cached generic plans. Keep custom_all unless the plan check is clean. # # single: rollback. One API pool on the previous settings; every guard family observes. # api_plan_layout: custom_all # Cost guards on the public /api/v1 group (not /admin, not /ws). Every family observes by default: # nothing is refused, only beacon_guard_would_refuse_total counts. Listed families enforce: # 429 (4 concurrent DB requests per client, 5 s wait), 503 (pool busy 5 s; class deadline 5/10/30 s). # Families: routes, channel_messages, packets_global, packets_region, packets_window, # packet_detail, stats, other. PUT /api/v1/admin/config changes the list at runtime; # a restart reloads this one. #guards: # enforce: [] # budget: shadow # the per-client DB-time budget only records; enforcement is not available # Daily read-only plan regression check (GET /api/v1/admin/db/planguard, beacon_planguard_* metrics). # Enable on large deployments; plan expectations assume production-sized tables. # Cannot be enabled together with server.read_only. #planguard: # daily_check: false # Read models. cmi is channel_message_iatas, one row per channel message and site that heard it; region # and scope message pages read it once this process trusts it. enabled: false serves every page from the # previous statements; the table is still maintained, so turning it back on needs only a restart. #readmodels: # cmi: # enabled: true # packet_sites: # # Serve region packet pages (iatas, region) from packet_sites once this process has repaired and verified # # it. false serves them from the original statement; the table is still maintained and swept. # enabled: true # MeshMapper integrations (optional, require a regional or grouped-region API key). # Your local MeshMapper regional or grouped-region admin can generate the key. # A grouped-region key covers multiple regions, including imported group members. # MESHMAPPER_API_KEY overrides meshmapper.api_key, even when empty. Use the # environment for secrets, especially with backups; never use the mobile App key. # Missing keys leave imports unconfigured; cached data and the rest of Beacon remain. # 401/403 failures retain cached data and report authentication/permission errors. # Import transport scopes and # IATA border outlines from MeshMapper instead of maintaining scopes: and # iatas.*.borderFile by hand. # # Scope-name discovery. Manual scopes remain authoritative. # Every known IATA, including ones auto-created from traffic, is looked up in its # country's zone list (shared with zones below) and its site's get_scopes.php # catalogue is imported; IATAs MeshMapper doesn't list are skipped. No sources # to configure; a legacy sources: list is ignored with a warning. # Catalogues covering more than one IATA are rejected. # 64 names per catalogue, 64 KiB per response; no page scraping. # Catalogues and ETags persist in PostgreSQL. Failed refreshes retain known names. # Refresh success/failure/freshness is logged under component=meshmapper.scopes. # Turning scopes off deactivates imported matching keys on restart; # historical scope identities and recorded evidence are retained. # Region-filtered scope lists and scope stats show imported names only under the # IATAs whose catalogue lists them; IATAs without a catalogue show manual scopes only. #meshmapper: # api_key: "" # scopes: # enabled: false # refresh_interval: 1h # 1h-24h (MeshMapper allows one get_scopes.php per region per 55m); one source per 15s tick # # Region boundaries (Zones API). Every known IATA, including ones auto-created # from traffic, is looked up in its country's zone list, then its get_geojson.php # outline is imported; new IATAs are picked up while running. An imported boundary # overrides that IATA's borderFile on the border map and for nodes.mark_foreign. # A missing or failed boundary keeps the last good import, or the borderFile when # there is none. Disabling drops every import on restart. Logged under # component=meshmapper.zones. # Each country's zone list is fetched at most once a day (MeshMapper allows one # get_zones.php per country per 23.5h) and saved, so restarts don't refetch it. # Failed or rate-limited requests also wait out the limit, or Retry-After if longer. # Listed IATAs also take their display name and approximate location from the # zone list, unless iatas: below sets a name or lat/lng for them. # import_groups turns MeshMapper zone groups (e.g. ONQC: YOW, YQB, YUL) into # regions, from the zone lists already fetched. A group is imported when at least # one member is heard by an observer or configured here, with its full member list (members Beacon hasn't # heard yet are added as IATAs); slug = lowercased group code, centered on the # members' average location, listed after the regions below. A region below with # the same slug wins. # Imported regions are removed when the group disappears or stops qualifying, # and all of them when import_groups is turned off. Requires zones.enabled. # zones: # enabled: false # import_groups: false # refresh_interval: 24h # 24h-168h (one get_geojson.php per region per 23.5h); one request per 15s tick, ETag-conditional # # Public hashtag channels (get_channels.php). Every known IATA that MeshMapper # lists is looked up on its site, the same way as scopes. Each key and hash is # checked against the name before use; a list with any mismatch is rejected and # the last good one kept. Imported channels decrypt like configured ones (history # is backfilled when a key first appears) and are listed under that IATA when # /channels is filtered by region. Saved in PostgreSQL across restarts; failures # wait out the 23.5h limit. Turning it off removes the region listing on restart. # Logged under component=meshmapper.channels. # channels: # enabled: false # refresh_interval: 24h # 24h-168h (one get_channels.php per region per 23.5h) # # Repeater site details (get_repeaters.php). Every known IATA that MeshMapper # lists is fetched from its site, the same way as channels. Each repeater's # admin-entered site details (power, hardware, antenna, heights, power source, # notes), CARpeater flag, admin names, confirmed scopes and default scope are # shown on node detail as `meshmapper`, matched by full public key. Positions, # status and radio data are not imported. A failed refresh keeps the last good # list and retries after 1h (or Retry-After); an empty list clears the region. # Each request counts against the key's shared budget (1,000/day, 30/min), so # 6h costs 4 requests per IATA per day. Turning it off deletes the import on # restart. Logged under component=meshmapper.repeaters. # repeaters: # enabled: false # refresh_interval: 6h # 1h-24h iatas: YVR: name: Vancouver International lat: 49.1967 lng: -123.1815 # borderFile: borders/yvr.geojson # optional GeoJSON Feature (Polygon/MultiPolygon) # # (or use meshmapper.zones above) # # for the region border map; path is relative to # # this file's directory; validated at boot. # # In Docker, also mount the folder: add # # ./data/app/borders:/app/borders:ro to the app service. YYJ: name: Victoria International lat: 48.6469 lng: -123.4260 YYC: name: Calgary International lat: 51.1139 lng: -114.0200 YEG: name: Edmonton International lat: 53.3097 lng: -113.5797 YOW: name: Ottawa International lat: 45.3225 lng: -75.6692 YYZ: name: Toronto Pearson lat: 43.6777 lng: -79.6248 # Hand-written regions; optional with meshmapper.zones.import_groups. Seeding # sets each region's members to exactly this list. regions: - slug: western-canada name: Western Canada display_order: 1 center_lat: 51.0 center_lng: -114.0 zoom_level: 5 iatas: [YVR, YYJ, YYC, YEG] - slug: eastern-canada name: Eastern Canada display_order: 2 center_lat: 45.0 center_lng: -75.0 zoom_level: 5 iatas: [YOW, YYZ] channel_keys: # Hashtag channels: Beacon derives the PSK automatically from the tag name. # secret = SHA256("#tag")[:16], channel_hash = SHA256(secret)[0] # Hashtag names should be provided without the # prefix. # Beacon prepends # automatically before deriving the key. # A plain name is listed under every region; add region: (a configured # region) to list it only there when /channels is filtered by region. hashtags: - meshcore # - name: ottawa-mesh # region: eastern-canada # Explicit keys: provide the channel hash (hex) and key (hex) directly. # An optional name can be set for display purposes. keys: "11": key: "8b3387e9c5cdea6ac9e5edbaa115cd72" name: "Public" # region: eastern-canada # optional, as for hashtags # Regional transport scopes for matching TRANSPORT_FLOOD packets. # Plain names have # prepended automatically (e.g. "bc" → "#bc"). # region (required) is a configured region slug; region-filtered scope lists # and scope stats show the scope under that region's IATAs. Matching stays global. # Optional when meshmapper.scopes covers your regions. scopes: - name: bc region: western-canada - name: "#west" region: western-canada telemetry: retention: 744h # 31 days resolution: 1h # store one snapshot per observer per hour # Optional authenticated database/config downloads. Disabled unless explicitly enabled. # Requires auth.api_key, a supported PostgreSQL URL and a compatible pg_dump client. # A failed prerequisite leaves this feature unavailable; ingest and other APIs continue. #backup: # enabled: false # Also covers observations and channel messages. packets: retention: 168h # 7 days # Hourly analytics rollups (stats endpoints). Rollups outlive raw packets; this is also the # longest window /stats/series and the other historical stats endpoints accept. analytics: rollup_retention: 2160h # 90 days # Presence write coalescing: observer last_seen and packet last_heard_at bumps are # batched in memory and flushed on an interval. An unclean shutdown loses at most # one interval of presence freshness. #presence: # flush_interval: 30s # default: 30s # packet_ttl: 30s # how long a quiet packet stays coalesced (default: 30s) # Known-route retention. Routes are distilled packet history (the path a packet # took, hop by hop), so they may outlive packets.retention; there is no required # relationship between the two. routes: # How long a route is kept after it was last observed. Every new observation # of the same path resets this clock, so routes the mesh still uses never # expire -- only paths nothing has traveled for this long age out. retention: 336h # 14 days # Shorter window for rarely-seen routes: one observed fewer than # min_observations times in total is dropped once it goes unobserved for # this long. Must be shorter than retention, or it never applies. grace: 168h # 7 days # Lifetime observation count a route must reach to earn the full retention # window. Below it a route is treated as flood noise -- a path recorded once # or twice and never confirmed -- and only kept for the grace window. The # count never resets, so once a route crosses this bar it stays in the # retention tier for good. min_observations: 3 websocket: max_connections_per_ip: 5 # default: 5 # Upgrade attempts, including failed handshakes; zero/omitted defaults to 10. # IPv6 clients share a /64 attempt budget. Negative values prevent startup. # Rate exhaustion rejects the handshake with 429 and Retry-After: 60. # Within this rate budget, a full concurrent cap accepts then closes with 1013 # before hello, allowing browsers to back off without evicting existing clients. max_connects_per_minute: 10 # Other sites allowed to open /ws. Exact scheme://host[:port], or # scheme://*.domain[:port] for every subdomain of domain at any depth. The # wildcard never matches the apex itself, and scheme and port stay exact. #allowed_origins: # - https://example.com # - https://*.example.com # Node staleness, deletion, and clock-drift thresholds. #nodes: # mark_foreign: false # opt in to possiblyForeign on repeater nodes # # uses the union of configured iatas.*.borderFile polygons, # # with meshmapper.zones boundaries replacing them; requires # # a valid border file or meshmapper.zones; no packet filtering # stale_threshold: 24h # default: 24h # delete_after: 720h # default: 30 days # clock_drift_threshold: 5m # default: 5m # Optional observer age-out. Omitted or nonpositive leaves observers intact. # Enable only with one Beacon ingest process for this database. # At most 1000 old observers are deleted per background.cleanup interval; # retained observations, telemetry and ownership records prevent deletion. #observers: # delete_after: 720h # opt in to 30 days unseen # Minimum client versions, published unauthenticated at GET /api/v1/info. # Older clients show an "update required" screen for this server. Each server # release has a built-in floor; these settings can raise it, never lower it. # Strict X.Y.Z (digits only, no "v" prefix or -beta suffix); any other value # prevents startup. Omitted or empty (default) means the built-in floor only. # Takes effect on restart. #mobile: # min_app_version: "0.1.1" # BEACON Mobile; built-in floor: none #web: # min_web_version: "2.0.3" # Beacon Web; built-in floor: 2.0.3 # CORS configuration. # Controls which origins, methods and headers are allowed for cross-origin requests. # Defaults to allowing all origins with GET/HEAD/OPTIONS if omitted, appropriate # for a public read-only API. Restrict allowed_origins if you expose write endpoints. #cors: # allowed_origins: # - "https://app.example.com" # allowed_methods: [GET, HEAD, OPTIONS, POST, PUT, DELETE] # admin UI writes # allowed_headers: [Accept, Authorization, Content-Type] # allow_credentials: false # max_age: 300 # Redis caching layer. # If REDIS_ADDR is not set, caching is disabled and all reads go directly to PostgreSQL. # TTLs are specified as duration strings e.g. "30m", "1h", "2h". # Per-category TTLs override the global ttl. Any category left unset inherits ttl. cache: # Global default TTL for all cached responses. Defaults to 1h if not set. ttl: "1h" ttls: # Aggregated stats endpoints (overview, observations, breakdowns, top nodes/observers, # radio presets, scope stats). Backed by hourly rollups; keys change when a new hour rolls. stats: "1h" # Mostly-static reference data (IATAs, regions, scopes). # Only changes when new observers arrive or config is reseeded. reference: "1h" # Individual node detail. Also explicitly invalidated on upsert. nodes: "1h" # Individual observer detail. Also explicitly invalidated on upsert. observers: "1h" # Geographic ingest filter (optional). # Drop packets from observers outside the specified area at ingest time. # Country codes are ISO 3166-1 alpha-2 (e.g. CA, US, GB). # Continent codes: AF (Africa), AN (Antarctica), AS (Asia), # EU (Europe), NA (North America), OC (Oceania), SA (South America). # If both are set an IATA passes if it matches either (OR semantics). # Omit this section entirely to accept packets from all IATAs (default). #ingest: # allow_countries: [CA, US] # allow_continents: [NA] # Background task intervals. # Shorter intervals are useful during initial deployment to confirm data is # flowing. Back off to 1h or more once stable. #background: # view_refresh: 1h # rebuilds radio presets; default: 1h # reconfirm: 1h # default: 1h # cleanup: 1h # default: 1h