Model Denmark's Dronebekendtgørelsen § 5 (on top of EU 2019/947) across all three tiers: schema, API Server, and Web App. Schema (migration 1720300700_add_logbook.js): two collections — `drones` (classification inputs: mtom, is_toy, autologs, c_class, operator no.) and `flights` (§5 minimum content + category/purpose/logging-path + operational maturity + a retention_until computed as operation_date + 5y). Locked API rules; access flows through the service account like users/orgs. API Server (logbook.go, logbook_export.go): /api/drones and /api/flights CRUD with per-role scoping in Go (user→own, admin→org, superadmin→all), plus GET /api/logbook/export (CSV — the "readable electronic format" for Trafikstyrelsen / pending police disclosure). Compliance is computed server-side per flight: exemption (toy / club-area / <250 g hobby), effective logging path, and red flags (autologs-without-FDR, specific-category-without- authorisation, missing §5 fields, past retention). Manual-path saves missing a §5 field are blocked (422). Web App: BFF proxies (export preserves the CSV Content-Type/Disposition), api.js client fns, and a Logbook.vue view (Flights/Drones tabs, inline forms, compliance badges + expandable detail, Export CSV) wired into Dashboard.vue, replacing the placeholder. Includes the rebuilt embedded dist bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PilotVault — API Server
Go service that is the single entry point for PilotVault. The Fly App streams drone telemetry to it; the Web App and the built-in API Web Panel read live state and issue commands. It keeps device state in memory and proxies authentication to a PocketBase kept behind it (PocketBase's address is never exposed to clients).
Fly App ──► /ws/device ┐
├─► API Server (:8080) ──► PocketBase (auth only)
Web App ──► /ws/ui ┘
The server root (GET /) serves a PilotVault-branded web panel: a live health
readout plus a quick reference of both API audiences. Open
http://localhost:8080/ in a browser to check the server at a glance.
The panel is a Vue 3 + Tailwind v4 app in panel/, built into
internal/api/dist and embedded into the Go binary at compile time:
cd panel
npm install
npm run build # outputs to ../internal/api/dist
cd ..; go build -o api-server.exe ./cmd/server # embeds the fresh dist
For panel development with hot reload (proxies /api to a running server on
:8080): cd panel; npm run dev → http://localhost:5174.
Requirements
- Go 1.24+ (
go version) - Node 18+ (only for building the panel)
- A reachable PocketBase instance with a
usersauth collection (for login). To persist user settings, that collection needs apreferencesJSON field — seepocketbase/README.md.
Configure
Copy-Item .env.example .env
# edit .env: set POCKETBASE_URL (and CORS_ALLOW_ORIGINS if needed)
| Variable | Purpose | Default |
|---|---|---|
API_ADDR |
Listen address | :8080 |
POCKETBASE_URL |
PocketBase base URL (login proxy). Legacy PB_URL still honoured. |
http://10.2.1.10:8026 |
CORS_ALLOW_ORIGINS |
Comma list, or * |
* |
Run
./scripts/Run-ApiServer.ps1
# or: go run ./cmd/server
Health check: GET http://localhost:8080/healthz.
API
Client / dashboard endpoints
| Method | Path | Description |
|---|---|---|
GET |
/healthz, /api/health |
Readiness probe + device count (public) |
POST |
/api/auth/login |
{email, password} → PocketBase session (proxied) |
GET |
/api/auth/validate |
Validate the Authorization token |
GET |
/api/me |
Caller's {id, email, role, organization, organizationName} resolved from their token |
GET |
/api/preferences |
Read the caller's saved settings blob (from their PocketBase user record) |
PUT |
/api/preferences |
{preferences} → persist the caller's settings onto their user record |
GET |
/api/users |
List users (manager; admin → own org, superadmin → all) |
POST |
/api/users |
{email, password, role, organization?} → create a user (manager; admin scoped to own org) |
PATCH |
/api/users/{id} |
Edit {email?, role?, verified?, password?, organization?} (manager; scope-checked; cannot demote self) |
DELETE |
/api/users/{id} |
Delete a user (manager; admin → own org only; cannot delete self) |
GET |
/api/orgs |
List organizations (manager; admin → own org, superadmin → all) |
POST |
/api/orgs |
{name} → create an organization (superadmin only) |
PATCH |
/api/orgs/{id} |
{name} → rename an organization (superadmin only) |
DELETE |
/api/orgs/{id} |
Delete an empty organization (superadmin only) |
GET |
/api/admin/pb-config |
Read the PocketBase connection + a live probe (superadmin only) |
POST |
/api/admin/pb-config/test |
{url?, adminEmail?, adminPassword?} → probe a candidate connection without applying (superadmin only) |
PUT |
/api/admin/pb-config |
{url, adminEmail?, adminPassword?} → apply at runtime + persist to .env (superadmin only) |
GET |
/api/admin/plugins |
List plugins with state + last health (superadmin only) |
POST |
/api/admin/plugins |
{name, baseURL, provider?} → register an external plugin, no rebuild (superadmin only) |
GET |
/api/admin/plugins/{name} |
One plugin's view (superadmin only) |
PUT |
/api/admin/plugins/{name} |
{enabled?, config?} → enable/disable + configure (superadmin only) |
DELETE |
/api/admin/plugins/{name} |
Remove an external plugin (superadmin only) |
POST |
/api/admin/plugins/{name}/health |
Run a health check now (superadmin only) |
GET |
/api/devices |
List devices and their last-known state |
GET |
/api/devices/{id}/track |
GPS track history for a device |
POST |
/api/devices/{id}/command |
Send {command, payload?} to a connected device |
DELETE |
/api/devices/{id} |
Forget a device's stored state |
GET |
/ws/ui |
Live telemetry stream (WebSocket) |
Device endpoints (Fly App)
| Method | Path | Description |
|---|---|---|
GET |
/ws/device?id={id} |
Telemetry uplink (WebSocket) |
POST |
/api/telemetry?id={id} |
Push a single telemetry event over HTTP |
Telemetry events
Device messages carry a type: registration, connection, battery, or
telemetry (altitude, lat/lng, velocity, GPS sats, flight mode…). The server
merges them into a per-device DeviceState and fans each update out to every
connected dashboard as {type:"update", device, event}. latitude/longitude
samples are appended to the device's GPS track.
Plugins
The server integrates external third-party services through a uniform plugin
contract (internal/plugins), managed by a superadmin from the panel. Two kinds
share one interface:
- Built-in — Go connectors compiled into the server (type-safe, first-party).
The reference example is OpenSky Network (
internal/plugins/builtin/opensky), a live ADS-B flight-state connector with an OAuth2 / anonymous auth provider. Adding a new built-in needs a rebuild. - External — a remote HTTP service registered at runtime, no rebuild. It
answers a small contract (
GET /health,GET /manifest,POST /invoke) and can run as its own process/container (the sandboxing story).
Enable-state and per-plugin config (secrets included) persist to a local,
gitignored plugins.json (override with PLUGINS_FILE), loaded on boot. Every
plugin exposes a real HealthCheck. Deferred extension points (invocation API,
retry/circuit-breaker, per-tenant credentials, audit logging) are documented in
internal/plugins/doc.go.
Writing a plugin: see the developer guide
internal/plugins/README.md — step-by-step for both
built-in (Go) and external (HTTP, no rebuild) plugins, with complete examples.
Project layout
cmd/server/main.go entry point, wiring, graceful shutdown
internal/config env/.env configuration
internal/hub in-memory device state + websocket fan-out (drone core)
internal/api router, middleware, handlers, embedded panel
internal/plugins plugin contract, manager, external kind + built-in connectors
panel/ Vue 3 + Tailwind v4 web panel (built into internal/api/dist)
scripts/ run helper