VATMissions is a flight mission and reward system for the VATSIM online flight simulation network. ARTCC administrators create missions (point-to-point flights between real-world airports), and registered pilots earn rewards by completing them on the live network. The system automatically tracks pilot progress in real time by polling the VATSIM data feed.
The project is a multi-service monorepo with three main components and a shared library:
vatmissions/
├── control/ # Background worker – polls VATSIM, tracks pilots, awards rewards
├── gateway/ # HTTP API server – authentication, mission CRUD, reward queries
├── shared/ # Go packages shared by control and gateway
├── app/ # Next.js frontend (React, TailwindCSS, shadcn/ui)
├── db/ # SQL schema and seed data
└── compose.yml # Docker Compose orchestration
VATSIM Data Feed (JSON)
│
▼
┌────────┐ ┌───────┐ ┌──────────┐
│ control│────▶│ Redis │◀────│ gateway │
└────────┘ └───────┘ └──────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Postgres │ │ app │
└──────────┘ └──────────┘
- control polls the VATSIM v3 data feed every 30 seconds.
- It matches online pilots to active missions by comparing departure/arrival ICAO pairs.
- Pilot tracking state is stored in Redis as a JSON-encoded state machine.
- When a pilot lands at the destination, control writes reward rows to Postgres.
- gateway serves the REST API that the app frontend consumes.
- gateway uses Redis for response caching and shares the same Postgres database.
The control service is a headless background worker with no HTTP endpoints. It runs two goroutines:
- DataFeedConsumerRoutine – Polls
https://data.vatsim.net/v3/vatsim-data.jsonevery 30 seconds and pushes parsed snapshots into a buffered Go channel. - DataFeedParser – Reads snapshots from the channel and executes the tracking pipeline:
- Load missions – Queries all non-expired missions and their airport coordinates from Postgres.
- Match pilots – Filters the feed to pilots whose
departure:arrivalICAO pair matches a mission route. - Exclude known – Removes pilots already tracked in Redis and known non-members (cached with 24h TTL).
- Membership check – Queries remaining candidates against the
userstable. Non-members are cached. - Begin tracking – New members on the ground at the departure airport enter the state machine.
- Update tracked – Every tracked pilot's state is recomputed. Only forward progression is allowed:
| State | Description |
|---|---|
0 – OnGroundAtDep |
Parked at departure (<5 nm, <100 ft AGL) |
1 – OffGroundNearDep |
Airborne, first 25% of route |
2 – OffGroundAtCruise |
Airborne, 25%–75% of route |
3 – OffGroundNearDest |
Airborne, last 25% of route |
4 – OnGroundAtDest |
Landed at destination (<5 nm, <100 ft AGL) |
- Award rewards – When state reaches
4, allmission_rewardsrows for that mission are inserted intouser_rewards, and the tracking key is removed. - Evict stale – Pilots not seen in the feed for >10 minutes are removed from tracking.
Distance calculations use the Haversine formula in nautical miles. "Percentage traveled" is derived from distFromDep / (distFromDep + distFromArr).
The gateway is a standard library net/http server listening on port 3100. It provides:
- Permissioned – Extracts and validates a JWT Bearer token, checks the caller's permission bitmask, and injects
VMClaimsinto the request context. - Cached – Redis-backed response caching with configurable TTL. Clients can bypass with
Cache-Control: no-cache.
| Method | Path | Permission | Description |
|---|---|---|---|
POST |
/register-user |
Public | Register a new user by VATSIM CID |
POST |
/authenticate |
Public | Exchange VATSIM CID for a JWT (1h expiry) |
GET |
/missions |
User | List all missions with rewards (cached 1h) |
GET |
/user-rewards |
User | List rewards earned by the authenticated user |
GET |
/artccs |
User | List all ARTCC names (cached 1h) |
GET |
/airports |
User | List all airports; ?artcc= to filter (cached 1h) |
POST |
/add-artcc |
Superuser | Create a new ARTCC |
POST |
/make-artcc-admin |
Superuser | Promote/demote a user to ARTCC admin |
POST |
/set-user-flags |
Superuser | Enable/disable permission bitmask flags |
POST |
/add-airport |
ARTCC Admin | Add an airport to an ARTCC |
POST |
/add-mission |
ARTCC Admin | Create a new mission |
POST |
/add-mission-reward |
ARTCC Admin | Attach a reward to a mission |
Note: There is currently no real authentication for demonstration purposes. The
/authenticateendpoint issues a JWT for any CID without verifying identity against VATSIM.
Permissions are stored as a 64-bit integer with bitwise composition:
| Bit | Value | Role |
|---|---|---|
0b001 |
1 | Superuser |
0b010 |
2 | ARTCC Admin |
0b100 |
4 | User |
A superuser has all bits set (0b111 = 7).
Shared Go packages used by both services:
cache_utils– Redis key conventions (tracked_pilots:<CID>,non_members:<CID>).db_types– Database row structs (Mission,MissionReward).gateway_types– Request/response structs with validation (Validatableinterface).http_utils– Generic JSON body reader, response writer, and error writer.jwt_utils– JWT creation, parsing, claims context injection, andPermissionstype.
A Next.js application with:
- TailwindCSS + shadcn/ui for styling and components
- Dark/light mode via
next-themes - Login page (authenticates by VATSIM CID)
- Dashboard with missions grouped by ARTCC
- Mission detail page with rewards
- User rewards page
The frontend proxies API requests to the gateway via Next.js rewrites.
PostgreSQL schema with the following tables:
artccs– ARTCC identifiers (e.g. "ZDV")airports– ICAO code, elevation, lat/lon, linked to an ARTCCmissions– Origin/destination airports, flight rules, constraints, expirationmission_rewards– Rewards attached to missions (title, description, image)users– VATSIM CID, permission bitmask, optional ARTCC admin assignmentuser_rewards– Join table linking users to earned rewards
- Docker and Docker Compose
./run.shThis will:
- Create the Docker network (if it doesn't already exist)
- Build and start all services via
docker compose up -d - Apply the database schema (
db/tables.sql) and seed data to Postgres
Once running, the services are available at:
- gateway –
http://localhost:3100 - control –
http://localhost:3101(no HTTP endpoints; background worker only) - Postgres –
localhost:5432 - Redis –
localhost:6379
You can pass any extra Docker Compose flags directly:
./run.sh --force-recreate --build