A real-time online airline management sim. One shared persistent world, running at 2× wall-clock time, that never pauses.
You start with one leased aircraft and one airport. You paint it, you fit the cabin, you pick a route, and you watch it fly — in real time, on a live world map, alongside every other player's fleet.
- Design document:
docs/tailfin-design-doc.md— the authority on every mechanic. If code and doc disagree, the doc wins. - Contributing & the four invariants:
CONTRIBUTING.md— read before writing anything. - Operating the project:
CLAUDE.md— deployment, environments, and the shared instructions for coding agents, including the rules that are not negotiable. - Architecture decisions:
docs/adr/ - Authorization boundary:
docs/authorization-matrix.md - Feature contracts:
docs/aircraft-acquisition.md·docs/aircraft-3d-assets.md·docs/aircraft-asset-pipeline.md·docs/aircraft-factory.md·docs/fleet-marketplace.md·docs/used-aircraft-market.md·docs/maintenance.md·docs/fleet-management.md·docs/belly-cargo.md·docs/fuel-pricing.md·docs/ground-handling.md·docs/training-academy.md·docs/hubs.md·docs/gates-and-stands.md·docs/world-renderer.md - Deployment & DNS:
docs/deploy.md·deploy/README.md - Roadmap: GitHub milestones are the live source for feature and cross-cutting work. Counts and track lists are not copied here because they change whenever the roadmap does.
- Roadmap dependencies:
docs/roadmap-dependencies.md— which domain owns what, what each depends on, and the contradictions that are still open. Relationships rather than progress, so it stays true as the milestones move.
Requires Node (version pinned in .nvmrc) and pnpm.
pnpm install
pnpm verifypnpm verify runs typecheck, lint, formatting, aircraft-asset validation, the production build, coverage tests and an
indicative performance pass in CI's cheap-fails-first order. It prints what passed and what
was skipped; CI remains authoritative for the protected merge checks. Its typecheck stage
also emits the declaration files that packages resolve each other through.
The database-backed tests skip without DATABASE_URL, and pnpm verify says so in its
summary rather than folding that gap into a pass. They are destructive by design and refuse
to run against any database whose name does not end in _test or _ci; the verifier also
distinguishes a refused URL from a disposable database it cannot reach.
packages/
assets/ deterministic aircraft intake — depends on shared + glTF tooling
shared/ types and zod schemas — depends on nothing
sim/ pure deterministic simulation — depends on shared only
server/ Fastify web + Worker, database — depends on shared, sim
web/ React client and admin console — depends on shared only
docs/
adr/ architecture decision records
deploy/ server runbook, systemd units, Caddy, backups
The dependency directions above are enforced by lint, not convention. packages/sim may
not import server, web or any node:* builtin; packages/web may not import sim.
Both rules exist so the simulation stays deterministic and the server stays authoritative
— see CONTRIBUTING.md.
| Check | Asks | Blocks? |
|---|---|---|
typecheck · lint · test |
Do builds, tests and the running Caddy policy pass? | Yes |
dependency review |
Did this PR add a known-vulnerable dependency? | High/critical advisories |
CodeQL analyze (…) |
Does Tailfin's own code contain a dangerous pattern? | Error or high/critical only |
main is protected and required approvals are zero — the pull request is the gate, not a
second person. CodeQL's measured thresholds and baseline decisions are recorded in
ADR-0013.
Pre-MVP, and pre-launch. The list below describes what is in the current repository; the
live milestone list owns sequencing and
completion status. The public site still serves a holding page: promoting the client is one
environment variable (WEB_SURFACE) plus a deploy, not a different build.
- The world clock.
epoch + speed × (now − launch_date), derived and never stored, so a reset is two columns and offline progression is free (ADR-0005). - Airport data. ~86,000 aerodromes imported from OurAirports, tiered, with catchment and a packed great-circle distance matrix.
- Flight mechanics, as pure functions. State machine with its failure branches, phase timeline, position interpolation along a great circle, and reachability and payload/range checks that name the limit that bound them.
- A flight's economics. Block time, phase-integrated fuel burn, a world fuel price
curve, turnaround, and settlement into an itemised
flight_resultwhen the aircraft lands — reconciled to the design doc's own published P&L to within a percent. - Rotations and schedules that survive a restart and an edit, and refuse the ones that assume an aeroplane can be in two places at once.
- Disruption and weather. Seeded per world and per flight so a replay reproduces them exactly, with climatological weather feeding delays, cancellations, diversions and de-icing.
- Demand pools. Appendix A.2's gravity model, sized for every viable city pair and split into business, leisure and VFR.
- Accounts. Google, Discord and Twitch OAuth — each independently configured, and an instance offers only the ones it holds credentials for — plus identity linking, database-backed sessions, atomic login rotation, immediate per-player revocation, shorter admin lifetimes, admin grants, and an append-only audit log the database itself refuses to let anyone edit (ADR-0015).
- One authorization error contract. A missing session is 401, a signed-in actor without a disclosed grant is 403, and a malformed, missing or cross-owner private resource is the same 404. Ownership is resolved inside the query so player endpoints cannot become object-existence oracles (ADR-0020).
- A repository-specific threat model. Security work prioritises the persistent world's integrity, then the identities and control paths that can change it. The model records the deployed web, worker, database, SSH and provider boundaries, attackers, ordinary operator mistakes and explicit non-goals in ADR-0012.
- A browser security boundary at Caddy. CSP restricts code, connections and framing; powerful unused browser features are denied; the three sign-in providers' avatar hosts are the only image-source exceptions, one per provider and nothing else. The edge rollout was observed in report-only mode before enforcement, both live hosts pass the enforced-policy verifier, and HSTS preload is deliberately deferred (ADR-0014).
- Recoverable off-box backups. Nightly DreamObjects dumps and their checksums are restored
repeatably into a guarded
_testdatabase, migrated, booted and checked against real domain data and the world clock, with observed recovery time and up-to-24-hour data loss stated in the server runbook. - Known migration failure states. PostgreSQL applies the complete pending migration batch atomically; future SQL is checked for expand/contract compatibility with the previous release, and a verified local dump gates every non-empty deploy batch. A failure reports rolled back, all applied, or unknown instead of implying that failed code means unchanged schema (ADR-0016).
- Airline founding. An authenticated player can found one airline in an open world,
choosing its identity, base country and first hub. Ownership, config-backed opening
cash, initial reputation and the free hub commit together or not at all; database
constraints arbitrate code collisions and return the submitted code in the refusal. The
no-menu
/founddesk reads those starting terms from the server, searches real tiered airports, warns without blocking an ambitious flagship choice, offers taken-code alternatives inline, and lands a successful founder on the network page. - A versioned airline starting position. Worlds pin an immutable, runtime-validated economy version that supplies opening cash and the free-hub allowance to founding. Unknown versions are refused when a world is created rather than when its first player arrives (ADR-0008).
- Explainable airline cash. Every game-balance change records its amount, cause, reference, game time and resulting balance in the transaction that caused it. Database constraints make cause replay idempotent and refuse any balance that does not equal the movement fold (ADR-0011).
- Race-safe airline code allocation. Founding allocates IATA and ICAO designators through the per-world unique constraints. An advisory checker and constraint refusals offer deterministic, name-derived alternatives without leaking unowned reservations (ADR-0009).
- One player-airline context boundary. Player operations resolve ownership from the authenticated session and active world before a handler runs, then query only inside that airline. A world is selected explicitly when several are possible; no-airline and ambiguous-world states have stable responses shared by every guarded endpoint (ADR-0010).
- Airline identity guardrails. One shared schema gives Unicode display names and operational callsigns/codes explicit rules, with field-level failures. A permissive moderation interface sits on both founding and an audited admin force-rename remedy; the stable airline id keeps its network and history attached through a rename (ADR-0007).
- A private airline record and paid rebrands. The owner can read current identity, stable codes, cash and reputation from one typed endpoint; having no airline is a normal discovery result. Players may change the validated name, callsign and base country for a versioned price, while codes, cash and reputation remain immutable inputs. The event, identity and reconciling cash movement commit atomically (ADR-0017).
- A retained airline lifecycle. Active airlines can make new commitments; restricted airlines remain recoverable and may operate what already exists; ceased airlines become read-only history. Cessation deactivates instructions but preserves flights, results and audit readability, releases designators from the live per-world namespace, and excludes the record from live caps and rankings. Player anonymisation removes sign-in authority while keeping that world history intact (ADR-0018).
- Three atomic aircraft acquisition paths. Active airlines can lease an immediately
available aircraft for a two-month deposit, buy a persisted used listing with its prior
configuration intact, or pay for a factory build whose pinned options extend a wall-clock
delivery date. The order, used-listing claim, explaining cash movement and any immediate
airframe commit together; the Worker materialises due new orders exactly once. The typed
API and its ownership boundary are documented in
docs/aircraft-acquisition.md. - Maintenance that bites. Airframes accrue block hours and cycles from every settled
flight; A, C and D checks fall due on whichever of the two limits arrives first, so a
short-haul turboprop and a long-haul widebody wear out differently. Deferring a check
raises the airframe’s technical-fault risk on a ramp rather than a cliff, and deferring it
half again past the limit grounds the aeroplane — which a schedule then refuses as a
conflict. See
docs/maintenance.md, including what the risk does not yet feed. - A used aircraft market with a history. A world offers a bounded, self-refilling
inventory of second-hand airframes, each with a build date, hours, cycles and a previous
owner’s configuration, priced by a depreciation curve and an unusual-configuration
discount. Every asking price arrives taken apart, so App. C.5’s claim that an odd
specification is cheap to buy is something a player can see rather than infer. The market
is the Worker’s, so it renews on dev only — see
docs/used-aircraft-market.md. - A fleet you can take apart. The Fleet page lists every airframe an airline owns, most
urgent first, with where it is, what it is doing with its time and which check binds next.
Opening one shows its effective specification option by option — the base value, the
amount each option actually moved it, and the running total — because a percentage from a
brochure and the kilograms an aeroplane really burns are not the same number. The
decomposition is arithmetically exact by construction and is tested against the airframe's
own stored spec. See
docs/fleet-management.md, including the two bulk actions that have nothing to act on yet. - Ground handling you can be wrong about. Every station offers three grades of handler on
six service lines, with finite capacity competing airlines exhaust — and since the milestone's
money landed, a grade changes what a turn costs as well as how fast and how reliably it
goes. So the cheap handler is a genuine choice rather than a strictly worse one, and handling
bought on the day is dearer than handling bought on a term. A contract runs a fixed term with
a volume commitment, bills a shortfall if the airline never flew what it promised, and costs a
pro-rated penalty to break — including by switching grades, because that is breaking one.
Self-handling is the alternative: it needs a hub and heads on a monthly payroll, lands just
short of a premium contractor when fully staffed and well below budget when it is not, and
trades a per-turn fee for a fixed cost that does not shrink when the schedule does. The term
and the payroll are the Worker's, so both bite on dev only — see
docs/ground-handling.md, including why there is no web UI yet. - Crew who get better at what you actually make them fly. Every completed flight awards
§10.2's XP to the crew aboard: a base figure from the sector length, a type factor from the
aeroplane's weight, and a difficulty multiplier built from how hard the fields are, the
weather they landed in, whether it was dark, whether they flew a diversion, and how far and
how oceanic the sector was. A hard winter northern network levels crew measurably faster
than easy domestic hops, which is the whole point — your route network shapes your crew, not
just your balance sheet. Airport difficulty is data:
pnpm data:difficultyderives a rating from runway length and elevation and raises it from a committed list of the famously hard fields, with an audit trail on every row. Every input is a stored or reproducible fact, so an old arrival re-derives the same XP and the factors that made it are written onto the flight result. Nothing spends it yet — levels and skill trees are M9-03. Seedocs/crew.md. - Crew who earn a name, and a skill tree you choose for them. §9.1 says never to manage
individuals; §10.2 is the sanctioned exception, and it is opt-in. Crew stay counts in a pool
until one crosses a level threshold, at which point a named person emerges from it with their
own experience, a career history and points to spend — four branches for a pilot, three for
cabin crew. The points make the airline cheaper and faster, never more popular: each one
feeds one of the design's six efficiency ceilings, stacked with diminishing returns and hard
capped, so a veteran airline is leaner rather than unbeatable. Type Mastery is the trade —
the biggest bonus, and it goes inert the moment you sell the fleet it was earned on, points
kept rather than refunded. Naming is the Worker's, so it bites on dev only, and the names
come from the world's seed so a replay produces the same roster. See
docs/crew.md, including why there is no respec and no public profile to put anybody on yet. - A training academy that gates a ceiling and grants no boost. §10.1's academy is built at
a crew base and climbs five levels, each unlocking a higher rank an airline may train, a
higher research tier it may later reach, and more of the finite training slots crew occupy
while they are in a classroom. Modules go in independently — a CBT suite, a cabin mock-up,
fixed-base and per-family full-flight simulators — and together they decide whether a type
conversion is trained in-house at a fraction of the market rate or simply bought in, which is
all the academy does to conversion: it is a discount and a ceiling, never a gate. Levelling
the building grants no performance bonus of any kind; that is the research and the boosts,
which are not built. Construction takes weeks of the world's calendar and no amount of money
shortens it, and both the commissioning and the monthly upkeep are the Worker's, so both bite
on dev only. See
docs/training-academy.md, including which modules are priced but inert and why the design doc names two different academies. - Fuel that costs what the station charges. Every airport prices its own Jet A-1: a
commodity factor for its region, an into-plane fee that scales with how hard the field is to
fuel, and a per-station spread fixed for the life of the world. A sector out of a Gulf hub
therefore costs measurably less to fuel than the same sector out of an African one, and the
world price itself walks a curve over the world's own calendar rather than sitting at its
opening level. Every number is
EconomyConfig, so retuning fuel is an audited re-pin and not a deploy — and unlike almost everything else in M4 and M5 this needs no Worker, because the curve is a pure function of a game instant. Seedocs/fuel-pricing.md, including why tankering is not built. - One founded-airline database fixture. Server tests that need a player airline go through the real founding transaction, so they receive an open world, owner, founder hub, allocated per-world codes and configured opening cash with its AIR-06 movement. The harness cleans only the row identities it created; no fixture truncates shared tables.
- One authorization test harness. Server route tests receive deterministic guest,
playerA, playerB and admin identities with real session cookies, declare all expected HTTP
statuses in one case, and clean only their own players. The Vitest setup also refuses every
configured database whose name does not end in
_testor_ci. - The admin console, at
/adminfor accounts holding a grant: an overview with server-decided alerts, world creation, speed changes, the full open/lock/archive/reset lifecycle, world health, a read-only player browser, linkable airline support records with current and historical routes and the complete paginated AIR-06 cash-movement ledger, an economy page listing every config version with which worlds pin it, a direct comparison of any two, and a re-pin that states what moves first, and the audit log. The airline record has no balance-edit control; it states instead whether its balance still equals the sum of its movements, and the overview raises an alert naming any airline where it does not. - One world renderer in two projections. The World page uses one deck.gl layer stack for
a repeating flat map and a 3D globe, with a persisted device-aware default, shared camera
and layer controls, bundled Natural Earth land, antimeridian-safe great-circle routes, and
sustained-FPS degradation. Day and night are a sampled darkness field uploaded as a
filtered texture, so the terminator is a smooth curve rather than a staircase of flat-shaded
cells. The renderer belongs to that page and no other: it was a shell-wide backdrop, which
meant every opaque page hid it while still paying for its WebGL context, and page content
took every drag aimed at the map. Its contract and performance policy are documented in
docs/world-renderer.md. - A deterministic 3D aircraft intake pipeline. Licensed source GLBs pass the official glTF
Validator plus Tailfin's transform, naming, material, UV, LOD, budget and no-external-resource
rules. Lossless optimisation is revalidated against the source contract; generated registry
paths, hashes, GPU estimates, review reports and projected source/runtime comparisons are
reproducible. CI rejects orphan runtime GLBs, while rollback changes the active version without
rewriting exact published livery bindings. The registry remains empty until the first licensed
asset is actually admitted — see
docs/aircraft-asset-pipeline.md.
- No production worker or complete flight-event lifecycle. The dev simulation runs in
dist/worker.js, separated from the web process by ADR-0019, but production has no worker. The Worker currently handlesFLIGHT_ARRIVEplus M4-04's real-time factory-delivery sweep; event types without a handler are parked asunsupportedrather than destroyed. The exact live topology is maintained inCLAUDE.md. - No fleet editing, crew or cabin management. The Fleet page now lists the airframes an airline owns beside the world's era-gated catalogue, and the aircraft detail takes an effective spec apart option by option. It is read-only: reconfiguring a build, editing a registration, booking a check from the page, and the used-market and order screens all remain to be built. Crew, ground handling, server-applied liveries and cabin management are future work. The versioned livery document contract and paired SVG templates for every launch aircraft family exist. The base-fill builder autosaves a validated airline-scoped local draft, but no livery is server-saved or applied yet — so an aircraft has no livery to show and no cabin fitted, and the fleet table says so rather than inventing either.
- Most of the player client. The standalone founding desk, private airline/rebrand desk, network/fare pages, aircraft catalogue, and dual-projection world surface are real. The world does not yet receive live aircraft/route data; finance, crew, design and board remain labelled placeholders, and the guided ninety-minute onboarding is still M10-01. The production front door still serves a holding page.
The canonical live node, service, database and deploy-command table is
CLAUDE.md's operational topology. It is
kept in one place so README does not become a second, stale topology after the next OPS
change. The deployment reasoning is in docs/deploy.md, and the exact
operator procedures are in deploy/README.md.
Merging does not deploy anything. Production moves only when somebody runs
./deploy/deploy.sh on the server, which is ADR-0003's deliberate choice and was
re-affirmed by OPS-06: merge means
staged, and a human promotes.
To see where things actually are, from anywhere and without an SSH session:
pnpm ops:statusCode: AGPL-3.0-only. Documentation: reserved (docs/LICENSE).
Copyright (C) 2026 Tailfinsim.
Tailfin is source-available and copyleft. You may read, run, fork and modify the code, and if you run a modified Tailfin as a service you have to offer your users its source — that is the AGPL's §13, and it is the reason this licence rather than the GPL. Tailfin is a hosted persistent world that nobody downloads, so plain GPL copyleft would essentially never trigger and a closed hosted fork would be permitted. The AGPL is what closes that.
docs/ is not covered. The design document and the ADRs are reserved, and that
is deliberate: a fork inherits the simulation, not the appendices that explain
why it behaves as it does.
Third-party terms — dependencies, the public-domain OurAirports dataset, and the
position on manufacturer names — are recorded in
THIRD-PARTY-NOTICES.md. No dependency is copyleft
and none is licence-incompatible.