Introduction
Contrail is a proposal to provide lexicons to describe flight segments. A contrail record
merely asserts that a particular flight existed, additional optional fields exist to
provide context. For instance, a user's connection to a particular flight (i.e. as a
passenger or pilot) is entirely optional. In fact, the only mandatory field required is
createdAt. This is the most important property of the schema and everything
else follows from it.
An absent field means the value is unknown or does not apply. It never means zero, empty, or none, and the schema defines no placeholder values. A record describing a flight where only the date and the tail number are known is as valid as one with every field populated. Tools that require complete data should enforce that requirement themselves, since the schema cannot do so without excluding general aviation, where most of the commercial fields have no meaning.
The relationship field is the one exception to this rule. When it is absent,
no claim is being made about the author's involvement, rather than the claim being
unknown. A record created from ADS-B data about another operator's aircraft simply omits
it.
Records for every kind of flying
Contrail is intended to accommodate commercial airline travel and general aviation equally. A widebody operating a transpacific sector and a Super Cub flying circuits at a grass strip are described by the same record type. The second has no flight number, no airline, and no airport code, and it is still complete.
{
"$type": "com.airplaneian.contrail.temp.flight",
"createdAt": "2026-07-25T09:14:00-07:00",
"date": "2025-07-13",
"origin": { "iata": "SFO", "terminal": "INTL", "gate": "A2" },
"destination": { "iata": "LHR", "terminal": "3" },
"operatingAirline": "VIR",
"flightNumber": "20",
"marketingAirline": "DAL",
"marketingFlightNumber": "5964",
"registration": "GVBOW",
"aircraftType": "Boeing 787-9",
"actualGateDeparture": "2025-07-13T15:57:00-07:00",
"actualTakeoff": "2025-07-13T16:25:00-07:00",
"actualLanding": "2025-07-14T10:14:00+01:00",
"actualGateArrival": "2025-07-14T10:23:00+01:00",
"relationship": "passenger",
"seat": "46K",
"status": "normal",
"source": "flighty",
"sourceId": "bef37d51-2876-433b-bffd-95c81f8fe569"
}
{
"$type": "com.airplaneian.contrail.temp.flight",
"createdAt": "2026-07-25T09:14:00-07:00",
"date": "2026-07-19",
"origin": {
"name": "Twin Oaks Airpark",
"faaLid": "7S3",
"geo": { "latitude": "45.42361", "longitude": "-122.94028" }
},
"destination": {
"name": "Twin Oaks Airpark",
"faaLid": "7S3",
"geo": { "latitude": "45.42361", "longitude": "-122.94028" }
},
"registration": "N12345",
"icao24": "a1b2c3",
"icaoTypeDesignator": "PA18",
"aircraftType": "Piper PA-18 Super Cub",
"actualTakeoff": "2026-07-19T10:12:00-07:00",
"actualLanding": "2026-07-19T10:58:00-07:00",
"relationship": "pic",
"source": "manual"
}
Note that the origin and destination in the general aviation example are the same place. Local flights are ordinary and the schema treats them as such.
Identifying flights across repositories
Because a record describes the flight rather than the author, several people aboard the same aircraft may each write a record for it. None of these is canonical, and Contrail treats that duplication as expected. It does not attempt to define a global flight identifier, because no authority exists to issue one.
This does mean readers need some way to recognise two repositories describing the same flight, so the identifying fields carry more weight here than they would in a personal logbook. Write them consistently wherever they are known.
| Commercial | date + operatingAirline + flightNumber |
|---|---|
| General aviation | icao24 + actualTakeoff |
The operatingAirline field takes the ICAO designator (UAL)
rather than the IATA code (UA). IATA airline codes are reassigned between
carriers over time, which makes a historical record tagged with one ambiguous.
Registrations are written in uppercase with no hyphens, so that G-VBOW and
GVBOW do not describe two different aircraft.
What Contrail leaves out
Duration, distance, and great circle mileage are all derivable from the times and places already present in a record. Storing them would guarantee that records written by different tools eventually disagree with each other.
Airport names, coordinates, and elevations are reference data about the world rather than
assertions by the author, so they do not belong in a personal repository. The one
exception is place.geo, which exists for landing sites that no identifier
system covers.
Times and time zones
Contrail defines eight timestamps, following the standard out, off, on and in convention, each with a scheduled and an actual variant. All of them are optional. A general aviation record will often carry only a takeoff and a landing.
Every timestamp is written with an explicit UTC offset and no fractional seconds. The Lexicon specification notes two round tripping ambiguities for datetimes, one involving lost precision and one involving trailing fractional second zeros, and a fixed convention avoids both.
Flighty importer demo
The demo below imports a Flighty CSV export into your repository as Contrail records. It runs entirely in this page. Your file is parsed in the browser and is never uploaded, because there is no server to upload it to.
Before you write anything
A complete flight history is also a movement history. Once these records are in your repository they are public and permanent, and a reader can work out where you are based, how often you travel, and when you are away from home.
This holds regardless of what the records claim. The relationship field
being optional does not change it, because records about flights sitting in your
repository disclose your connection to those flights either way.
Definitions
The following schemas are published to did:plc:5jsivedaksbcc4n2epiegflk and resolve by NSID.
com.airplaneian.contrail.temp.flight, the flight record itselfcom.airplaneian.contrail.temp.trip, which groups flights into a journey-
com.airplaneian.contrail.temp.defs, holding the sharedplacedefinition
These are currently published under a temp namespace while the shape
settles. Published lexicon constraints can never be loosened afterwards, so the stable
names will not be claimed until real data has been through them.