PJET is a local API and browser frontend for exploring private-jet takeoff and landing pairs derived from ADS-B Exchange heatmap data. It builds compact SQLite databases from a local cache, then serves authenticated queries and emissions estimates.
A complete historical rebuild is a serious data operation, not a quick demo. The builder reads 30-minute heatmap files and a full cache is very large.
Be considerate of ADS-B Exchange. Obtaining or backfilling a complete cache can make more than 1 TB of requests and downloads from ADS-B Exchange. Do not launch parallel bulk fetches, respect the provider's terms and rate limits, and obtain permission before attempting a historical backfill. Reuse an authorized local cache whenever possible.
Using an accessible URL does not grant rights to publish the resulting data or a derived public service. Before a deployment, confirm that your ADS-B Exchange agreement permits the specific acquisition, retention, derivative-event, and publication use you intend. The provider's service terms place restrictions on mass export and redistribution unless they are expressly authorized.
Generated data is intentionally ignored by Git. Do not commit or publish a cache,
SQLite database, auth database, update status file, or screenshots. In particular,
aircraft.sqlite can contain FAA registrant fields, and auth.sqlite contains
credential hashes and session-signing material.
- Node.js 20 or newer
- Python 3.12 or newer with uv
- Rust, only when using the faster Rust event builder or the incremental updater
- An ADS-B Exchange heatmap cache to build events
Install the application dependencies:
uv sync
npm ciBy default, PJET writes all generated files under the ignored data/ directory:
data/
pjet/ # events.sqlite, airports.sqlite, aircraft.sqlite, build_state.sqlite, auth.sqlite
adsbx/ # YYYY/MM/DD/SS.bin.ttf heatmap cache
aircraft/ # optional ADS-B Exchange, tar1090, and OpenSky metadata inputs
faa/ # optional FAA ReleasableAircraft.zip input
For a historical rebuild, keep the large cache on an external or attached volume. The location is deliberately configurable rather than embedded in the code:
# One root for all ignored local data.
export PJET_DATA_DIR=/path/to/large-volume/pjet-data
# Or override individual locations when the cache and databases are separate.
export PJET_DB_DIR=/path/to/database-volume/pjet
export PJET_ADSBX_CACHE_DIR=/path/to/heatmap-cache
export PJET_AIRCRAFT_CACHE_DIR=/path/to/aircraft-inputs
export PJET_FAA_ZIP=/path/to/faa/ReleasableAircraft.zipPJET_DB_DIR is also used by the Node server and Rust builder. The Python builder
uses the same variables, so one configuration works across the toolchain.
First build the small reference databases. Airport data is downloaded from OurAirports. Aircraft metadata inputs are optional individually, but supplying authoritative source files is strongly recommended for useful classifications:
uv run pjet import-airports
uv run pjet import-aircraft \
--faa-zip "$PJET_FAA_ZIP" \
--adsbx-basic-db "$PJET_AIRCRAFT_CACHE_DIR/basic-ac-db.json.gz" \
--tar1090-db "$PJET_AIRCRAFT_CACHE_DIR/tar1090-aircraft.csv.gz" \
--opensky-csv "$PJET_AIRCRAFT_CACHE_DIR/opensky-aircraft-database.csv"Then process an existing heatmap cache. Start with a narrow period while verifying your setup:
uv run pjet build-events --start 2026-05-01 --end 2026-05-06 --resetOmit --start and --end only when you intentionally want to process every file
available in the configured cache. The builder checkpoints processed files and keeps
rolling aircraft state in build_state.sqlite, so subsequent runs add only newly
completed events.
The Rust implementation writes the same event and state schemas and is recommended for large historical rebuilds:
cd rust/pjet
cargo build --release
cd ../..
./rust/pjet/target/release/pjet build-events --start 2026-05-01 --end 2026-05-06 --resetThe incremental updater in scripts/update-events.mjs downloads missing
30-minute slots and should only be used after the database and state files have
been initialized. It is not a substitute for permission to bulk-download history.
PJET does not redistribute EUROCONTROL Small Emitters Tool workbooks or derived fuel-curve data. EUROCONTROL states that SET equations are subject to a specific data-use agreement. If you have obtained the workbook under terms that allow your use, export an ignored local CSV and configure it before starting the server:
uv run --with openpyxl python scripts/export-eurocontrol-set-csv.py \
/path/to/authorized-set-workbook.xlsx \
data/eurocontrol.csv
export PJET_EUROCONTROL_CURVES="$PWD/data/eurocontrol.csv"data/eurocontrol.csv is also the default local path, so the export command works
without the environment variable when run from the repository root. Without an
available local curve file, PJET still supports the Adapted Travel Impact Model and
the Combined Model's ATIM fallback; the EUROCONTROL Fast and Slow choices are
omitted.
The vendored flight-emissions code is Apache-2.0, but its bundled datasets retain
their own provenance and licensing. For example, the OpenFlights airport database is
published under the Open Database License and may require attribution and a compatible
license for public database derivatives. Review every upstream dataset and its current
terms before distributing a generated database or offering it through a public API.
The FAA import can include registrant names and location fields. Even when a source is public, decide deliberately whether exposing those fields is appropriate for your deployment and jurisdiction.
Create the initial admin password in the environment and start the API. A new
auth.sqlite refuses to start without PJET_PASSWORD; there is no default
credential. The password is stored as a salted hash on first start, so later changes
to the environment variable do not replace it.
export PJET_PASSWORD="$(openssl rand -base64 32)"
HOST=127.0.0.1 PORT=8080 npm run serveOpen http://127.0.0.1:8080/. Use /admin to create or revoke journalist
vouchers. A voucher value is displayed only when it is created, so save it securely.
For an internet-facing deployment, keep the Node process behind HTTPS and add
network-level request limits in the reverse proxy or edge service. PJET also applies
a small in-memory login-failure limit, but that is not a replacement for perimeter
protection.
workers/d1-api.js is an alternative API backend, not part of the local server
above. It fails closed until a deployment configures the PJET_API_TOKEN Worker
secret; send that token as Authorization: Bearer …. Do not put the token in
wrangler.toml, frontend code, or a commit.
The current complete events.sqlite is about 31.7 GB (29.5 GiB). Plan for at
least 100 GB of fast SSD storage for the database, indexes, WAL files, and operating
headroom; 200 GB or more is more comfortable if you retain backups or expect growth.
The production reader/updater currently runs successfully on a DigitalOcean Basic Droplet with 2 vCPUs, 4 GB RAM, and 120 GB disk. That is a practical minimum for serving the existing database. For a new public service, 4 vCPUs, 8 GB RAM, and 200 GB+ NVMe storage provide better query and maintenance headroom. A complete historical cache needs a separate large volume—budget at least 2 TB—and a 4-vCPU, 16-GB-RAM machine is a sensible starting point for a full rebuild.
All API routes except login and session status require an authenticated admin or voucher session.
GET /api/airports?minLat=&minLon=&maxLat=&maxLon=&type= returns airports inside a
rectangular bounding box. type is optional and matches an airport type label such
as heliport, small_airport, or large_airport.
POST /api/events accepts:
{
"start": "2026-05-01T00:00:00Z",
"end": "2026-05-06T00:00:00Z",
"airportIds": [123, 456],
"aircraftType": "business_jet"
}It returns matching events, airport operation counts, and emissions estimates.