PHP proxy for FAA NOTAM data backed by a local database cache.
The web endpoint no longer calls the FAA API on user requests. Instead:
reconcile_notams.phpperforms a full GeoJSON refresh by classification.sync_notams.phpapplies FAA delta updates usinglastUpdatedDate.notam.phpserves user queries from the local database.status.phpreports cache freshness and record counts.notam_live.phpoptionally exposes a direct FAA-backed comparison endpoint.notam_compare.phpoptionally compares DB-backed and live FAA results for the same query.faa_connectivity_test.phpverifies FAA auth and a small authenticated API probe.
This keeps the FAA request rate decoupled from user traffic.
GET:
/notam.php?locationLongitude=a&locationLatitude=b&locationRadius=c
Required:
locationLongitude(alias:lon) = longitude of the search centerlocationLatitude(alias:lat) = latitude of the search centerlocationRadius(alias:radius) = search radius in nautical miles (0-100)
POST JSON body with known IDs and lastUpdated timestamps:
{"known":{"NOTAM_ID":"lastUpdated","NOTAM_ID_2":"lastUpdated"}}Response contains only new or changed items plus removedIds.
Top-level fields:
totalCountitems(GeoJSON Feature list)
Delta mode adds:
deltaremovedIds
The endpoint also returns sync metadata in headers:
X-NOTAM-Source: databaseX-NOTAM-Last-Delta-SyncX-NOTAM-Last-Full-ReconcileX-NOTAM-Sync-Age-Seconds
If the local cache is too old, notam.php returns 503 instead of serving indefinitely stale data. During an in-progress full reconcile, the service allows a temporary reconcile grace window before failing closed.
DB_SERVERDB_PORTdefault:3306DB_NAMEDB_USERDB_PASSFAA_IDFAA_SECRETFAA_API_BASEdefault:https://api-nms.aim.faa.gov/nmsapiFAA_AUTH_URLdefault:https://api-nms.aim.faa.gov/v1/auth/tokenNMS_RESPONSE_FORMATmust beGEOJSONFAA_RECONCILE_CLASSIFICATIONSdefault:INTERNATIONAL,MILITARY,LOCAL_MILITARY,DOMESTIC,FDCNOTAM_MAX_SYNC_AGE_SECONDSdefault:900NOTAM_RECONCILE_GRACE_SECONDSdefault:21600NOTAM_RECONCILE_MEMORY_LIMITdefault:1024Mfor CLI reconcile runsNOTAM_ALLOW_LIVE_PROXYoptional boolean to enablenotam_live.phpNOTAM_LIVE_PROXY_KEYshared secret required in production whennotam_live.phpornotam_compare.phpis enabledFAA_CONNECTIVITY_KEYshared secret required in production forfaa_connectivity_test.phpNOTAM_ENV_FILEoptional absolute path to a.envfile outside the document rootAPP_ENVdefault:production
The application loads .env automatically on both web and CLI entry points. Real process environment variables still take precedence.
In production, do not keep .env in a publicly reachable web directory. Prefer setting NOTAM_ENV_FILE to a path outside the document root. If that is not possible, your web server must explicitly deny access to .env.
- Configure the environment variables in
.env. - Run
php reconcile_notams.phponce to bootstrap the local store. - Schedule
php sync_notams.phpevery 3 minutes. - Schedule
php reconcile_notams.phponce per day. - Check
status.phpto confirm the cache is healthy. - Point clients at
notam.php.
reconcile_notams.php builds a full staging snapshot and atomically swaps it into place, so readers should not see a mixed dataset during the daily refresh.
Run the local regression suite with:
php tests/run.phpUseful local helper commands:
chmod +x scripts/check_remote.sh
chmod +x scripts/check_local_mysql.sh
scripts/check_remote.sh
LOCAL_DB_USER=your_local_db_user LOCAL_DB_PASS=your_local_db_password scripts/check_local_mysql.shIf remote MySQL writes from a separate machine are too slow, you can build the cache into a local MySQL database and export it for import into production:
chmod +x scripts/build_local_dump.sh
chmod +x scripts/import_remote_dump.sh
LOCAL_DB_USER=your_local_db_user LOCAL_DB_PASS=your_local_db_password scripts/build_local_dump.shImportant variables:
LOCAL_DB_SERVERdefault:127.0.0.1LOCAL_DB_PORTdefault:3306LOCAL_DB_NAMEdefault:xcsoar_notam_localLOCAL_DB_USERdefault:rootLOCAL_DB_PASSdefault: emptyLOCAL_DUMP_PATHdefault:tmp/xcsoar_notam_local.sql
This script reads credentials from .env. It writes the local cache to a local MySQL database, then dumps notam_cache, notam_items, and notam_state to a SQL file for import elsewhere.
To import that dump into the remote production database from the CLI:
scripts/import_remote_dump.shImportant variables:
ENV_FILEdefault:.envDUMP_FILEdefault:tmp/xcsoar_notam_local.sqlDB_SERVER_OVERRIDEdefault:server2.febas.netIMPORT_FORCEdefault:0MYSQL_EXTRA_ARGSdefault: empty
The import helper reads the remote DB credentials from .env, prompts before replacing notam_cache, notam_items, and notam_state, and supports both .sql and .sql.gz dump files.
If your remote MySQL server has a broken or expired TLS certificate and the local MariaDB client refuses the connection, pass explicit client flags, for example:
MYSQL_EXTRA_ARGS='--skip-ssl-verify-server-cert' scripts/import_remote_dump.shIf the server does not accept TLS cleanly at all and allows plaintext connections, use:
MYSQL_EXTRA_ARGS='--skip-ssl' scripts/import_remote_dump.shstatus.php returns JSON health information for the local cache:
- whether the cache is initialized
- whether the cache is stale
- sync age in seconds
- active and total row counts
- last delta and reconcile timestamps
faa_connectivity_test.php performs:
- FAA auth token POST
- one authenticated FAA checklist probe
It does not print the bearer token. In production, FAA_CONNECTIVITY_KEY must be set or the endpoint stays disabled. When configured, the endpoint requires ?key=... or the X-Connectivity-Key header.
notam_live.php can be enabled separately to fetch directly from the FAA API for response comparison against the DB-backed notam.php.
This should stay disabled by default. In production, enabling it without NOTAM_LIVE_PROXY_KEY keeps the endpoint disabled. Use it only for controlled comparison, not normal client traffic.
notam_compare.php uses the same guard and returns a structured comparison between cached and live results, including:
- cached count
- live count
- IDs only present in cache
- IDs only present from live FAA data
- IDs present in both but with different payloads
Optional query parameter:
mode=normalized(default): compares cached and live after applying local geometry + active-status filtering on both sidesmode=raw: compares unnormalized sets (notam_fetch_local_featuresvs FAA response)mode=both: returns both normalized comparison (comparison) and raw comparison (comparisonRaw)
The scripts will create the required tables automatically if the database user has permission. If you prefer to create them up front:
CREATE TABLE notam_cache (
cache_key VARCHAR(128) NOT NULL,
cache_value LONGTEXT NOT NULL,
expiration DATETIME NOT NULL,
PRIMARY KEY (cache_key),
KEY idx_expiration (expiration)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE notam_state (
state_key VARCHAR(128) NOT NULL,
state_value LONGTEXT NOT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (state_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE notam_items (
notam_id VARCHAR(64) NOT NULL,
last_updated VARCHAR(40) NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
classification VARCHAR(32) DEFAULT NULL,
effective_start DATETIME DEFAULT NULL,
effective_end DATETIME DEFAULT NULL,
min_lat DOUBLE DEFAULT NULL,
max_lat DOUBLE DEFAULT NULL,
min_lon DOUBLE DEFAULT NULL,
max_lon DOUBLE DEFAULT NULL,
full_sync_run VARCHAR(64) DEFAULT NULL,
payload LONGTEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (notam_id),
KEY idx_active_bounds (is_active, min_lat, max_lat, min_lon, max_lon),
KEY idx_last_updated (last_updated),
KEY idx_full_sync_run (full_sync_run)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;See CRON.md.