79 KiB
Pilot Logbook Database — Design Document
Status: Draft v0.1 — design phase, pre-DDL
Target platform: PostgreSQL 16+
Scope: Multi-user, audit-bearing pilot logbook with open interchange format
Table of Contents
- Project Overview
- Design Philosophy
- Regulatory Framework
- High-Level Architecture
- Schema Organization
- Entity Reference
- Category-Specific Flight Details
- Time Modeling
- Audit Trail Design
- Entry Locking and Corrections
- Cryptographic Signatures
- Lookup Tables and Enums
- Views
- Book Reports
- Constraints and Indexes
- Import / Export
- Migrations
- Backup Strategy
- Out of Scope (Explicit)
- Future Considerations / Open Questions
- Glossary
- Appendix A — 14 CFR 61.51 Requirements Mapping
- Appendix B — Regulatory Reference Index
- Appendix C — High-Level Entity Relationship Diagram
1. Project Overview
1.1 Purpose
A PostgreSQL database schema for pilot flight logbooks that:
- Satisfies 14 CFR 61.51 logging requirements as a regulatory floor.
- Supports computation (via queries and views) of compliance status for currency, recency, and aeronautical-experience regulations across Part 61.
- Maintains a court-quality audit trail of all logbook modifications.
- Supports cryptographic signing of entries for instructor verification.
- Operates as a multi-user system with strict per-user data isolation.
- Exports to an open, documented interchange format to escape proprietary lock-in.
1.2 Goals
- Regulatory correctness: every required 61.51 element captured atomically; downstream regulation satisfied by query.
- Audit-bearing: every change captured with who, when, what, and why, with tamper-evidence.
- Open standard: define and publish the JSON interchange schema; permit and encourage adoption by other projects.
- Multi-aviator: support airplane, helicopter, glider, balloon, and other categories without forcing one-size-fits-all.
- Future-proof: prefer semantic honesty over short-term schema simplicity. Wide-but-correct beats narrow-with-nulls.
1.3 Non-Goals
See section 19 for a full enumeration. Briefly: duty/rest time calculation, aircraft maintenance tracking, scheduling, and dispatch are explicitly out of scope.
2. Design Philosophy
2.1 Atomic Facts, Derived Compliance
Store the smallest indivisible facts about each flight; compute everything else by query. A "cross-country qualifying flight" is not a stored attribute — it's a query result based on stored leg distances and airports. A "currency status" is not a stored attribute — it's a function of stored flights filtered by date and aircraft category.
This principle has cascading effects throughout the schema:
- Time categories (PIC, SIC, dual, solo, night, instrument) are stored per flight.
- Aircraft attributes (complex, high-performance, tailwheel, type-rating-required) are stored on the aircraft model, not the flight, and inherited via join.
- Currency is never stored — always computed.
- The pilot's "totals" are aggregate queries, not maintained counters.
This costs query complexity and benefits correctness. Counters drift; aggregate queries do not.
2.2 Append-Only Spirit with Selective Locking
Paper logbooks are physically append-only. Once you've signed your name and the CFI has signed theirs, the entry is fixed; corrections take the form of explicit subsequent entries that reference the error.
This schema preserves that discipline selectively:
- Unlocked entries support normal updates. The audit log captures every change.
- Locked entries (signed by a CFI, or explicitly locked by the user) cannot be modified in place. Corrections require a new entry that references the original.
- Erroneous entries are marked, not deleted. The original remains in the historical record; views exclude them from totals.
2.3 Open Standard
The export format is intended to be a community standard, not just this implementation's backup file. To that end:
- The JSON export schema is documented, versioned, and licensed permissively (CC0 for the spec).
- The schema is designed for round-trip fidelity: export then re-import must reproduce identical logbook state, including audit history and signatures.
- Identifiers are globally unique (UUID v7) so logbooks can be merged or migrated across instances without collision.
2.4 PostgreSQL-Native
This schema is written for PostgreSQL 16+ and uses PostgreSQL-specific features without apology:
- Partial unique indexes (e.g., "exactly one owner per logbook")
- JSONB for flexible attribute storage
- Generated columns for derived per-row fields
- Row-level security for multi-tenant isolation
- Triggers for audit log maintenance
- Range types where temporal queries are natural
- Schemas (namespaces) for organization
No abstraction layer or ORM is assumed. Cross-database portability is explicitly not a goal.
3. Regulatory Framework
3.1 14 CFR 61.51 — Pilot Logbooks (The Floor)
This is the regulation that directly governs pilot logbook content. Every flight log entry must include (per §61.51(b)):
- General: date; total flight or lesson time; location where flight originated and terminated (or the location of a ground/sim lesson); type and identification of aircraft, FFS, FTD, or ATD.
- Type of pilot experience or training: solo; PIC; SIC; flight and ground training received from authorized instructor; training received in FFS/FTD/ATD from authorized instructor.
- Conditions of flight: day or night; actual instrument; simulated instrument in flight, FFS, FTD, or ATD.
Per §61.51(c)-(e), additional logging applies to specific operations: flight instruction received and given (§61.51(e), (h)), instrument approaches and instrument time (§61.51(g)), instructor records (§61.51(h)), and the safety pilot's name when applicable.
Per §61.51(i), logbook entries must be available for inspection by the Administrator, an authorized representative of the NTSB, or any federal, state, or local law enforcement officer.
Per §61.51(j), instruction must be signed by the authorized instructor providing it; the instructor must include their certificate number and expiration date.
3.2 Related Regulations
These regulations impose requirements that are derived from logbook contents via query:
- §61.1 — Definitions, including cross-country flight (>50 NM variant for most ratings), night flight (end of evening civil twilight to beginning of morning civil twilight — the logging definition).
- §61.31 — Type rating, complex, high-performance, tailwheel, high-altitude, and pressurized aircraft requirements; required endorsements.
- §61.55 — SIC qualifications; familiarization training and recent flight experience for SIC in type-certificated multi-pilot aircraft.
- §61.56 — Flight review (every 24 calendar months).
- §61.57 — Recent flight experience: passenger-carrying (90-day takeoff/landing in category/class/type, day and night), instrument (6 approaches + holding + tracking in preceding 6 calendar months), with grace periods and IPC requirements after lapse.
- §61.65 — Instrument rating aeronautical experience.
- §61.129 — Commercial pilot aeronautical experience.
- §61.159 — ATP aeronautical experience (1500-hour rule and subcategories).
The "night" definition for §61.57(b) currency differs from the §1.1 definition for logging night time. The schema accommodates both.
3.3 Related FAA Guidance
- AC 120-78B — Acceptance and Use of Electronic Signatures, Electronic Recordkeeping Systems, and Electronic Manuals.
- FAA Order 8900.1 — Flight Standards Information Management System (FSIMS).
Compliance with AC 120-78B drives the design of the cryptographic signature subsystem (section 11).
4. High-Level Architecture
4.1 PostgreSQL-Specific Design
PostgreSQL 16 or later. Features used liberally:
- Multiple schemas for namespace organization
- JSONB columns for user-defined and extensible attributes
- Generated columns (STORED) where the source data lives in the same row
- Partial indexes (e.g., on locked entries, on recent flights)
- Range types for temporal validity
- Triggers (BEFORE INSERT, AFTER INSERT/UPDATE/DELETE) for audit and integrity
- Row-Level Security (RLS) for multi-user isolation
pgcryptofor hashing in the audit chain and for UUID v7 generation (or built-inuuidv7()in PG18+)
4.2 Multi-User Model
The system is multi-user from the database's perspective. Each user has their own logbook. No data is shared between users' logbooks — not flights, not people records, not aircraft instances. The mental model is one-database-per-installation-with-many-tenants.
Reference data (airports, aircraft model definitions, training device model definitions, FAR citations, lookup-table contents) is shared and read-only to users. Administrators populate it; users reference it.
Per-user tables carry an owner_user_id column. Row-level security policies enforce owner_user_id = current_user_session_id() for all access.
4.3 Single Logbook Owner Per User
Each user owns exactly one logbook (their own). Within the user's people table, exactly one row represents the logbook owner themselves — the pilot the logbook is for. This is enforced by a partial unique index:
CREATE UNIQUE INDEX one_owner_per_user
ON logbook.people (owner_user_id, is_logbook_owner)
WHERE is_logbook_owner = TRUE;
The user appears in their own people table as a regular row, distinguished only by the is_logbook_owner flag. This pattern keeps the schema consistent: the owner is "just another person" in the data model, special only by virtue of the boolean.
4.4 Identifiers
All primary keys are UUID (v7, time-ordered). UUID v7 is preferred over v4 because:
- It sorts naturally by creation time, keeping B-tree index inserts efficient.
- It carries a millisecond timestamp embedded in the value, useful for forensic ordering.
- It's globally unique, so logbooks can be exported, imported, and merged across instances without collision.
Surrogate UUIDs are used universally; natural keys (FAA certificate numbers, N-numbers, ICAO airport codes) appear as separate uniquely-indexed columns where they exist.
4.5 Timestamps
All timestamp columns are timestamptz (timestamp with time zone). Aviation is inherently multi-timezone; storing local times without zones invites data corruption. Display-layer conversion to local time happens at the application layer.
Date-only columns (e.g., flight date for logging purposes) use date. Dates do not carry time zones; a flight logged on 2025-03-15 is logged on that date regardless of what time zone the pilot was in.
For events that need timestamp precision (takeoffs, landings, push-back times), timestamptz is used and the moment is stored with full UTC offset information.
4.6 Numeric Precision
Time durations are NUMERIC(5,1) — tenths of an hour, up to 9999.9. This matches pilot logbook convention (entries are written in tenths) and avoids floating-point accumulation errors. The maximum 9999.9 hours per single entry is more than any real flight; per-pilot totals fit easily in NUMERIC(7,1) aggregated.
Money or currency is not part of this schema.
Distances are NUMERIC(7,2) nautical miles; great-circle calculations from airport lat/lon use the haversine formula in a SQL function. PostGIS is not used.
5. Schema Organization
The database uses four PostgreSQL schemas (namespaces) for logical separation:
5.1 reference
Shared, read-only data, populated by administrators. Same content for all users. No owner_user_id columns. No RLS.
Contains:
airports— global airport referenceaircraft_models— aircraft type/model definitionstraining_device_models— simulator and training device type definitionsfar_citations— regulation reference data- Lookup tables:
aircraft_categories,aircraft_classes,approach_types,certificate_levels,rating_types,endorsement_types,medical_classes,role_on_flight_types,training_device_types,pic_basis_types,landing_event_types, etc.
5.2 logbook
Per-user logbook data. Every table carries owner_user_id and is subject to RLS policies.
Contains entities described in detail in section 6: people, person_roles, aircraft, training_devices, flights, flight_legs, training_sessions, landings_takeoffs, approaches, flight_crew, certificates, ratings, certificate_limitations, endorsements, medicals, flight_reviews, practical_tests, training_events, attachments, public_keys, entry_signatures, book_reports, tags, entry_tags, category-specific extension tables (helicopter_flight_details, glider_flight_details, etc.).
5.3 audit
Audit log. Append-only at the role level (INSERT permitted, UPDATE and DELETE revoked). Hash-chained for tamper-evidence.
Contains:
audit_log— all changes to audited tables inlogbookschema- Possibly auxiliary tables for change-reason templates or session-tracking metadata.
5.4 auth
User authentication and identification. Distinct from logbook.people — a users row is an account that logs in; a people row is a person in the logbook (which may be the user themselves, or someone else they fly with).
Contains:
users— authentication records, profile metadata- Session-related tables if implemented in-database (could also be application-layer)
6. Entity Reference
This section describes every entity in the schema. Each subsection covers:
- Purpose — what this entity represents
- Key fields — the columns that define it
- Relationships — foreign keys and references
- Constraints — important rules
- Rationale — why it's modeled this way
6.1 auth.users
Purpose: A user account that logs into the system. One per human who has their own logbook.
Key fields:
id(UUID v7, PK)email(text, unique)display_name(text)password_hash(text) — or alternative auth scheme referencesis_active(boolean)created_at,updated_at(timestamptz)last_login_at(timestamptz, nullable)
Relationships: Referenced by owner_user_id columns throughout the logbook schema.
Notes: Authentication mechanism (password, OAuth, etc.) is an application-layer concern. This table provides the database-layer identity that RLS policies key against.
6.2 logbook.people
Purpose: Any person who appears in the user's logbook — the owner themselves, instructors, fellow pilots, examiners, flight attendants, dispatchers, anyone the user has flown with or trained under.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)is_logbook_owner(boolean, default false)first_name,middle_name,last_name(text)display_name(text, optional override)faa_certificate_number(text, nullable) — primary FAA airman certificate numbernotes(text)created_at,updated_at(timestamptz)
Constraints:
- Partial unique index ensuring exactly one
is_logbook_owner = TRUEperowner_user_id.
Notes: Biographical only. Role/qualification information lives in person_roles. The user's own row has is_logbook_owner = TRUE.
6.3 logbook.person_roles
Purpose: Time-ranged record of roles, qualifications, certificates that a person has held. Captures "John was a CFI from 2005 to 2018" or "Mary held an ATP starting 2020 (still current)."
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)person_id(UUID, FK →logbook.people)role_type(text, FK →reference.role_qualification_types) — e.g., 'CFI', 'CFII', 'MEI', 'ATP', 'flight_attendant', 'dispatcher', 'A&P_mechanic', 'DPE'identifier(text, nullable) — qualification-specific number, e.g., CFI certificate numbervalid_from(date, nullable) — NULL means "unknown but presumed prior to first observation"valid_to(date, nullable) — NULL means "currently held / no known end"notes(text)created_at,updated_at(timestamptz)
Notes: NULL dates explicitly mean "unknown" — approximate dates were rejected in design (audit clarity > convenience). Multiple person_roles rows per person are expected; the same person may have CFI, MEI, and ATP rows, possibly with different validity periods.
6.4 reference.airports
Purpose: Global airport reference. Shared across all users.
Key fields:
id(UUID v7, PK)icao_code(text, nullable, unique-where-not-null)iata_code(text, nullable)faa_lid(text, nullable) — FAA Location Identifier (e.g., "0B5" for fields without ICAO)name(text)municipality(text, nullable)country_code(text) — ISO 3166-1 alpha-2latitude(numeric(9,6))longitude(numeric(9,6))elevation_ft(integer, nullable)has_control_tower(boolean) — current state; historical state captured per landingtime_zone(text) — IANA tz name (e.g., "America/New_York")is_active(boolean) — for decommissioned airportscreated_at,updated_at(timestamptz)
Rationale: Lat/lon needed for great-circle distance (cross-country qualification) and for sunrise/sunset calculation (night currency). Time zone needed for converting timestamptz events to local for night determination. has_control_tower is current state; the per-landing was_at_towered_field captures historical truth.
6.5 reference.aircraft_models
Purpose: Aircraft type/model reference. A Cessna 172S is one row; an individual N-number flying that model references this row.
Key fields:
id(UUID v7, PK)manufacturer(text) — "Cessna", "Cirrus", "Boeing"model_name(text) — "172S Skyhawk", "SR22T G6", "737-800"icao_type_designator(text, nullable) — "C172", "B738", "SR22"category_id(UUID, FK →reference.aircraft_categories) — airplane, rotorcraft, glider, lighter-than-air, powered-lift, weight-shift-control, powered-parachuteclass_id(UUID, FK →reference.aircraft_classes) — single-engine land, multi-engine land, single-engine sea, multi-engine sea, helicopter, gyroplane, balloon, airshipengine_type(text) — piston, turboprop, turbojet, turbofan, electric, none-gliderengine_count(smallint)gear_type(text) — tricycle, conventional (tailwheel), skids, skis, floats, amphibiousis_complex(boolean) — retractable + flaps + controllable-pitch prop (§61.31(e))is_high_performance(boolean) — >200 HP (§61.31(f))is_tailwheel(boolean) — derived from gear_type but stored for indexabilityis_pressurized(boolean)service_ceiling_ft(integer, nullable) — for §61.31(g) high-altitude flagging if >25,000 ft pressurizationrequires_type_rating(boolean) — large aircraft (>12,500 lb MGTOW), turbojet-powered, or specifically listedtype_rating_code(text, nullable) — e.g., "B737", "CE-525S"mtow_lbs(integer, nullable) — Maximum Takeoff Weightnotes(text)created_at,updated_at(timestamptz)
Notes: Many flagging attributes (is_complex, is_high_performance, etc.) live on the model, not the flight. A flight in an Arrow automatically counts as complex time via join. This eliminates a class of data-entry error.
6.6 logbook.aircraft
Purpose: A specific aircraft instance — a particular N-number (or other-country registration).
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)model_id(UUID, FK →reference.aircraft_models)registration(text) — "N12345", "G-ABCD", "C-FXYZ"serial_number(text, nullable)year_manufactured(smallint, nullable)nickname(text, nullable) — user's name for the aircraftis_glass_cockpit(boolean, default false) — installed equipmentis_ifr_certified(boolean, default true)equipment_notes(text)notes(text)created_at,updated_at(timestamptz)
Constraints: (owner_user_id, registration) unique within an owner's logbook.
Notes: Two users flying the same N-number have two distinct rows (per the strict no-sharing principle). Equipment is an aircraft-level attribute; a flight in a G1000-equipped 172 is automatically "G1000 time."
6.7 reference.training_device_models
Purpose: Training device type reference. A model of FFS, FTD, ATD, BATD, or AATD.
Key fields:
id(UUID v7, PK)manufacturer(text)model_name(text)device_type_id(UUID, FK →reference.training_device_types) — FFS, FTD, ATD, BATD, AATD, "unknown_historical"device_level(text, nullable) — "Level A", "Level D", "Level 7", etc., per the era's classification scheme; nullableaircraft_model_simulated_id(UUID, FK →reference.aircraft_models, nullable) — many ATDs simulate generic aircraftnotes(text)created_at,updated_at(timestamptz)
Notes: Per design discussion, device level is nullable. Historical training in pre-2008 devices may have no level information available; this is an acceptable state. device_type_id can also reference "unknown_historical" for very old records.
6.8 logbook.training_devices
Purpose: A specific training device instance — a particular sim at a particular facility.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)model_id(UUID, FK →reference.training_device_models)facility_name(text, nullable)facility_location(text, nullable) — free text; not all sims are at registered airportsserial_number(text, nullable)faa_qualification_letter(text, nullable)notes(text)created_at,updated_at(timestamptz)
6.9 logbook.flights
Purpose: The header for a flight — a logical flight composed of one or more legs. For single-leg flights this is functionally one row with one leg child.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)flight_date(date) — the date the flight is logged onaircraft_id(UUID, FK →logbook.aircraft) — required (this table is for actual aircraft only)pic_basis_id(UUID, FK →reference.pic_basis_types) — sole_manipulator, acting_pic, atp_part_121, more_than_one_pilot_required- Time category columns, each
NUMERIC(5,1)andNOT NULL DEFAULT 0:total_timepic_timesic_timedual_receiveddual_given(instruction given by the pilot, if a CFI)solo_timecross_country_time(the simple "cross-country" flavor)cross_country_50nm_time(the >50 NM flavor for most ratings, §61.1)night_time(§1.1 logging definition: end of civil twilight)actual_instrumentsimulated_instrument(in-aircraft with view-limiting device)
- Block timestamps (all nullable timestamptz):
block_out— pushback from gate / start of movementblock_off— wheels off (takeoff)block_on— wheels on (landing)block_in— arrival at gate / shutdown
- Flight conditions (all nullable booleans, denormalized for query speed):
holding_performedcourse_intercepting_tracking_performed
- CFI signature fields (simple, for "instructor signed this off" cases):
cfi_signature_person_id(UUID, FK →logbook.people, nullable)cfi_signed_at(timestamptz, nullable)
- Locking and corrections:
is_locked(boolean, default false)locked_at(timestamptz, nullable)locked_reason(text, nullable) — 'cfi_signature', 'user_locked', 'cryptographic_signature'corrects_flight_id(UUID, FK self-reference, nullable)correction_reason(text, nullable)superseded_by_flight_id(UUID, FK self-reference, nullable) — denormalized; trigger-maintainedis_erroneous(boolean, default false)erroneous_reason(text, nullable)marked_erroneous_at(timestamptz, nullable)
- Free-form:
remarks(text) — the pilot's notes, freely edited (unlocked entries) or via correction (locked entries)
- Provenance:
created_at,updated_at(timestamptz)created_by_user_id(UUID, FK →auth.users) — usually equalsowner_user_id; future-proofing for admin or assistant cases
Constraints:
total_time >= 0- Each time category column
>= 0 - All time category columns
<= total_time + 0.1(allowing for rounding) where logically applicable- Note: PIC + dual_received can both be > 0 simultaneously and may both equal total_time in CFI-receiving-instruction scenarios; the constraints need careful crafting and are documented inline in DDL.
(corrects_flight_id IS NULL) OR (correction_reason IS NOT NULL)(is_erroneous = FALSE) OR (erroneous_reason IS NOT NULL)- Block-time ordering: if both endpoints present, end > start.
block_out < block_off < block_on < block_inwhere all are present.
Notes: total_time is the canonical §61.51 logged time and is stored, not generated. The application UI may compute it from block times and prefill, but the user can override. The schema does not enforce a relationship between block fields and total_time because too many edge cases exist (return-to-gate aborts, taxi-back flights with no actual flight).
6.10 logbook.flight_legs
Purpose: Individual legs within a flight. A KPHL → KLNS → KABE → KPHL day is one flights row with three flight_legs rows.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)flight_id(UUID, FK →logbook.flights)leg_number(smallint) — 1-based orderingfrom_airport_id(UUID, FK →reference.airports, nullable) — nullable for off-airport departuresto_airport_id(UUID, FK →reference.airports, nullable) — nullable for off-airport landings, diverts to unlisted fieldsfrom_airport_description(text, nullable) — for off-airport cases ("Smith family farm field")to_airport_description(text, nullable)distance_nm(numeric(7,2)) — typically generated; haversine from airport coordsleg_time(numeric(5,1)) — pilot-logged time for this legnotes(text)created_at,updated_at(timestamptz)
Constraints:
(flight_id, leg_number)uniqueleg_time >= 0
Notes: A single-leg flight has one row here with leg_number = 1. The application auto-creates this row on flight insert if only origin/destination are provided. The user is shielded from the legs abstraction unless they need it.
Multi-leg sums: SUM(flight_legs.leg_time) WHERE flight_id = X should approximately equal flights.total_time. The schema does not enforce strict equality (rounding) but a sanity check trigger could warn.
6.11 logbook.landings_takeoffs
Purpose: Individual takeoff and landing events. Each row represents one event; touch-and-goes are a single row with both is_takeoff = TRUE and is_landing = TRUE.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)flight_leg_id(UUID, FK →logbook.flight_legs)airport_id(UUID, FK →reference.airports, nullable) — nullable for off-airport eventsairport_description(text, nullable) — for off-airport casesoccurred_at(timestamptz, nullable) — precise time; needed for sunrise/sunset determinationis_takeoff(boolean, not null, default false)is_landing(boolean, not null, default false)is_full_stop(boolean, nullable) — meaningful only for landingswas_at_towered_field(boolean, nullable) — captured at time of event (regulation cares about then, not now)runway(text, nullable)notes(text)created_at,updated_at(timestamptz)
Derived (via view or generated column):
is_day_per_part1— based onoccurred_atand airport coordinates, end of civil twilight definitionis_night_per_6157— based onoccurred_atand airport coordinates, 1-hour-after-sunset / 1-hour-before-sunrise definition
Constraints:
is_takeoff OR is_landing(one event minimum)(is_full_stop IS NULL) OR (is_landing = TRUE)(full stop only meaningful with landing)
Notes: This is the granular source of truth for landing/takeoff counts. Summary counts on flight_legs and flights (e.g., day_landings, night_landings_full_stop) are denormalized convenience fields maintained by triggers. A day takeoff that crosses into night before landing is correctly represented as two rows: takeoff row (is_day_per_part1 = TRUE), landing row (is_night_per_6157 = TRUE).
Sunrise/sunset computation is implemented in a SQL/PLpgSQL function based on the NOAA solar position algorithm, taking lat/lon and a date.
6.12 logbook.approaches
Purpose: Instrument approaches logged for §61.51(g) and §61.57(c) currency.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)flight_leg_id(UUID, FK →logbook.flight_legs, nullable) — for in-aircraft approachestraining_session_id(UUID, FK →logbook.training_sessions, nullable) — for sim approachesapproach_type_id(UUID, FK →reference.approach_types) — ILS, LOC, LOC-BC, VOR, NDB, RNAV-GPS-LNAV, RNAV-GPS-LPV, RNAV-GPS-LNAV/VNAV, GLS, PAR, ASR, etc.airport_id(UUID, FK →reference.airports, nullable)runway(text, nullable)in_imc(boolean) — actual instrument conditionsin_simulated_imc(boolean) — view-limiting device or simwas_circling(boolean)was_missed(boolean) — flew the missed approach procedureto_landing(boolean) — landed off this approachnotes(text)created_at,updated_at(timestamptz)
Constraints:
- Exactly one of
flight_leg_idortraining_session_idis non-null.
6.13 logbook.training_sessions
Purpose: Training in a "bolted-to-the-earth" device (FFS, FTD, ATD, BATD, AATD). Distinct from flights in actual aircraft.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)session_date(date)training_device_id(UUID, FK →logbook.training_devices)- Time columns (NUMERIC(5,1)):
total_timeinstrument_time_simulated(per §61.51(g)(5), sim instrument time logged as simulated)dual_receiveddual_given
instructor_person_id(UUID, FK →logbook.people, nullable)holding_performed(boolean, nullable)course_intercepting_tracking_performed(boolean, nullable)- Locking and corrections (same pattern as flights):
is_locked,locked_at,locked_reasoncorrects_training_session_id,correction_reason,superseded_by_training_session_idis_erroneous,erroneous_reason,marked_erroneous_at
- CFI signature fields (same pattern as flights)
remarks(text)created_at,updated_at,created_by_user_id
Notes: No airport, no landings, no takeoffs in the regulatory sense. Approaches done in a sim are logged via the approaches table with training_session_id set. Time logged here does not count for §61.57(a) takeoff/landing currency under any interpretation; queries that compute such currency must filter on flights only.
6.14 logbook.flight_crew
Purpose: Junction table tying people to specific flights with their role on that flight.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)flight_id(UUID, FK →logbook.flights, nullable)training_session_id(UUID, FK →logbook.training_sessions, nullable)person_id(UUID, FK →logbook.people)role_on_flight_id(UUID, FK →reference.role_on_flight_types)seat_position(text, nullable) — left, right, jumpseat, cabinnotes(text)created_at,updated_at(timestamptz)
Constraints:
- Exactly one of
flight_idortraining_session_idis non-null.
Notes: Role types include: PIC, SIC, CFI_giving_instruction, student_receiving_instruction, safety_pilot, flight_attendant, jumpseat_observer, relief_pilot, IRO, check_airman, examiner, dispatcher_observer, mechanic_observer, etc. Same person can have different role_on_flight_id values on different flights — captures the "John was PIC on flight A, SIC on flight B" case naturally.
6.15 logbook.certificates
Purpose: FAA airman certificates held by the logbook owner.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)certificate_level_id(UUID, FK →reference.certificate_levels) — Student, Sport, Recreational, Private, Commercial, ATP, Flight Instructor, Ground Instructor, Remote Pilot, Mechanic, etc.certificate_number(text)date_of_issuance(date)date_of_first_issuance(date, nullable) — for re-issued certificatesissuing_authority(text, default 'FAA') — for non-FAA certificates (ICAO conversion, foreign)notes(text)created_at,updated_at(timestamptz)
6.16 logbook.ratings
Purpose: Ratings attached to a certificate. A Commercial certificate may carry SEL, MEL, Instrument-Airplane, and a type rating like B737.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)certificate_id(UUID, FK →logbook.certificates)rating_type_id(UUID, FK →reference.rating_types)date_added(date)notes(text)created_at,updated_at(timestamptz)
6.17 logbook.certificate_limitations
Purpose: Limitations endorsed on a certificate. "Limited to centerline thrust," "VFR only," "Not valid for international flight," etc.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)certificate_id(UUID, FK →logbook.certificates)limitation_text(text)date_added(date)date_removed(date, nullable) — limitations can be removed by subsequent checkridenotes(text)created_at,updated_at(timestamptz)
6.18 logbook.endorsements
Purpose: Instructor endorsements per §61.31, §61.39, §61.56, §61.57(d), and others. Distinct from certificates and ratings.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)endorsement_type_id(UUID, FK →reference.endorsement_types) — high_performance, complex, tailwheel, high_altitude, type_specific, solo, solo_cross_country, flight_review, IPC, etc.endorsement_date(date)endorsing_cfi_person_id(UUID, FK →logbook.people)cfi_certificate_number_at_time(text) — captured at time of endorsementcfi_certificate_expiration_at_time(date, nullable)far_reference(text) — the regulation citationendorsement_text(text) — full text as written by CFIaircraft_model_id(UUID, FK →reference.aircraft_models, nullable) — for model-specific endorsementsexpiration_date(date, nullable) — for endorsements with currency implicationsnotes(text)created_at,updated_at(timestamptz)
6.19 logbook.medicals
Purpose: Medical certificates held. Supports Class 1, 2, 3 traditional medicals, BasicMed, and Special Issuance.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)medical_class_id(UUID, FK →reference.medical_classes) — Class 1, Class 2, Class 3, BasicMed, Special Issuanceexam_date(date)ame_person_id(UUID, FK →logbook.people, nullable) — for AME-issuedexpiration_date_for_class_1_ops(date, nullable) — computed by application or storedexpiration_date_for_class_2_ops(date, nullable)expiration_date_for_class_3_ops(date, nullable)is_special_issuance(boolean, default false)special_issuance_conditions(text, nullable)basicmed_cmec_date(date, nullable) — Comprehensive Medical Examination Checklist date (for BasicMed)basicmed_course_date(date, nullable) — BasicMed online course completionpilots_medical_certificate_number(text, nullable)notes(text)created_at,updated_at(timestamptz)
Notes: Multiple expiration date columns reflect the fact that a Class 1 also confers Class 2 and Class 3 privileges, each expiring at a different time depending on the pilot's age. Application logic computes these from exam_date and pilot age; storing them allows fast queries.
6.20 logbook.flight_reviews
Purpose: §61.56 flight reviews and their equivalents (new certificate/rating issued in preceding 24 months, IPC that includes the review elements, WINGS phase).
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)review_date(date)review_type(text) — 'flight_review', 'new_rating', 'IPC_combined', 'WINGS_phase', 'proficiency_check'instructor_person_id(UUID, FK →logbook.people)associated_flight_id(UUID, FK →logbook.flights, nullable)aircraft_or_device_description(text)satisfactorily_completed(boolean)endorsement_id(UUID, FK →logbook.endorsements, nullable) — link to corresponding endorsementnotes(text)created_at,updated_at(timestamptz)
6.21 logbook.practical_tests
Purpose: Checkrides and practical tests.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)test_date(date)examiner_person_id(UUID, FK →logbook.people)certificate_or_rating_sought(text)outcome(text) — 'pass', 'fail', 'discontinuance', 'notice_of_disapproval'aircraft_id(UUID, FK →logbook.aircraft, nullable)training_device_id(UUID, FK →logbook.training_devices, nullable)associated_flight_id(UUID, FK →logbook.flights, nullable)associated_training_session_id(UUID, FK →logbook.training_sessions, nullable)acs_or_pts_reference(text, nullable)resulted_in_certificate_id(UUID, FK →logbook.certificates, nullable) — for passing testsresulted_in_rating_id(UUID, FK →logbook.ratings, nullable)notes(text)created_at,updated_at(timestamptz)
Notes: Examiner could be FAA inspector, DPE, ACR. The associated aircraft or training device records what the test was conducted in. Both being NULL is valid (e.g., oral exam retake).
6.22 logbook.training_events
Purpose: Training events of all kinds — recurrent training, type ratings programs, AQP, CQT, §61.55 familiarization training, ground school, etc. Supports future-dated planned events with unknown specifics.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)event_type_id(UUID, FK →reference.training_event_types) — recurrent_training, AQP, CQT, type_rating_initial, type_rating_recurrent, §61.55_familiarization, ground_school, OE_initial, line_check, etc.status(text) — 'planned', 'scheduled', 'in_progress', 'completed', 'cancelled'planned_period_description(text, nullable) — "April 2027" or similar when exact dates unknownplanned_start_date(date, nullable)planned_end_date(date, nullable)actual_start_date(date, nullable)actual_end_date(date, nullable)provider(text, nullable) — "FlightSafety International", "CAE", employer namelocation(text, nullable)aircraft_type(text, nullable) — "Boeing 737NG"satisfactorily_completed(boolean, nullable)notes(text)created_at,updated_at(timestamptz)
Notes: This is intentionally flexible. The "April 2027 training is scheduled but no specific dates" scenario uses status = 'planned', planned_period_description = 'April 2027', all date columns NULL. Updates as schedule firms up.
6.23 logbook.attachments
Purpose: File attachments — scanned medical certificates, endorsement photos, 8710 forms, training certificates, anything else.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)owner_table(text) — name of the table the attachment is linked to (flights,endorsements, etc.)owner_row_id(UUID) — UUID of the row inowner_tablefilename(text) — original filenamemime_type(text)file_size_bytes(bigint)storage_path(text) — object storage key or filesystem pathsha256_hash(bytea) — content hash for integritydescription(text)uploaded_at(timestamptz)created_at,updated_at(timestamptz)
Notes: The (owner_table, owner_row_id) pair is a soft foreign key — not enforced by the database, validated by the application. This polymorphic pattern is accepted here despite the integrity tradeoff because attachments are loosely coupled and the alternative (one attachments table per attachable entity) is worse.
File contents live in object storage (S3-compatible like MinIO for self-hosted) or on disk, not in the database. The sha256_hash allows detection of file corruption or tampering.
6.24 logbook.tags and logbook.entry_tags
Purpose: User-defined tags for flights and other entries. Supports niche tracking (aerobatic time, mountain flying, formation flight, specific routes, etc.) without schema changes.
logbook.tags:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)tag_name(text)tag_color(text, nullable) — for UI displaydescription(text)tag_value_type(text, default 'boolean') — 'boolean' (presence-only), 'numeric_hours' (aggregable hours), 'text' (descriptive)created_at,updated_at(timestamptz)
Constraint: (owner_user_id, tag_name) unique.
logbook.entry_tags:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)tag_id(UUID, FK →logbook.tags)owner_table(text) —flights,training_sessions,flight_legsowner_row_id(UUID)numeric_value(numeric(5,1), nullable) — for hours-type tagstext_value(text, nullable) — for text-type tagscreated_at,updated_at(timestamptz)
Notes: Three tag value types support different use cases. A boolean tag like "formation_flight" is just presence. A numeric tag like "aerobatic_time" stores hours, allowing aggregations like "total aerobatic time." A text tag like "checkride_examiner_notes" captures descriptive metadata.
6.25 logbook.public_keys
Purpose: Registered public keys for people who sign entries (typically CFIs, but potentially anyone). Backs the cryptographic signature subsystem.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)person_id(UUID, FK →logbook.people)key_fingerprint(text)key_algorithm(text) — 'rsa', 'ed25519', 'pgp', etc.public_key_data(text) — ASCII-armored public key or PEMkey_format(text) — 'pgp_armored', 'x509_pem', 'openssh', etc.registered_at(timestamptz)revoked_at(timestamptz, nullable)revocation_reason(text, nullable)notes(text)created_at,updated_at(timestamptz)
Constraint: (owner_user_id, key_fingerprint) unique.
6.26 logbook.entry_signatures
Purpose: Cryptographic signatures over logbook entries.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)entry_table(text) —flights,training_sessions,endorsements,book_reportsentry_id(UUID) — soft FK to row inentry_tablesigner_person_id(UUID, FK →logbook.people)public_key_id(UUID, FK →logbook.public_keys)canonical_form_version(text) — version of the canonicalization algorithm usedcanonical_data_hash(bytea) — SHA-256 of canonical bytes signedsignature_data(bytea) — the signature itselfsigned_at(timestamptz)signature_purpose(text) — 'cfi_endorsement_signature', 'student_acknowledgment', 'report_attestation', etc.notes(text)created_at,updated_at(timestamptz)
Notes: Signing an entry sets is_locked = TRUE on the entry via trigger. Modifications to a locked entry are rejected, preserving signature validity.
6.27 logbook.book_reports
Purpose: On-demand snapshots of currency, expirations, totals, and other derived data, generated at a moment and persisted. Reproducible historical reports.
Key fields:
id(UUID v7, PK)owner_user_id(UUID, FK →auth.users)generated_at(timestamptz)as_of_date(date) — the date the report content is computed againstreport_type(text) — 'currency_summary', 'next_due', 'totals', 'pre_flight_check', 'experience_summary', etc.report_data(jsonb) — the computed report contentsnotes(text)- Attestation fields:
is_attested(boolean, default false)attested_at(timestamptz, nullable)attested_by_person_id(UUID, FK →logbook.people, nullable)attestation_text(text, nullable) — the actual statement attested toattestation_signature_id(UUID, FK →logbook.entry_signatures, nullable)
created_at,updated_at(timestamptz)
Notes: A function generate_book_report(user_id, as_of_date, report_type) runs the underlying queries and inserts a row. Reports for personal use have is_attested = FALSE. Reports for external presentation (job interview, FAA inspection response) go through an attestation step, optionally cryptographically signed.
6.28 audit.audit_log
Purpose: Tamper-evident record of every change to audited tables.
Key fields:
id(bigserial, PK)table_name(text)row_id(UUID)operation(text) — 'INSERT', 'UPDATE', 'DELETE'changed_at(timestamptz)changed_by_user_id(UUID, nullable)session_user(text) —current_userat time of changechange_reason(text, nullable) — set viaSET LOCAL audit.change_reason = '...'before UPDATE/DELETEold_values(jsonb, nullable) — full row before changenew_values(jsonb, nullable) — full row after changeprev_hash(bytea) — hash of the immediately prior audit_log rowrow_hash(bytea) — SHA-256 of(prev_hash || canonical content of this row)
Access: INSERT only at the application role level. UPDATE and DELETE privileges revoked.
Notes: See section 9 for detailed mechanics.
7. Category-Specific Flight Details
Different aircraft categories have different relevant metadata. Rather than burdening flights with NULL columns for irrelevant categories, we use a subclass table pattern: a one-to-one optional extension table per category.
7.1 Pattern
Each subclass table:
- Has
flight_id(UUID) as both PK and FK tologbook.flights. - Is created only when category-specific data needs to be recorded.
- Has its own columns for category-specific attributes.
The presence of a row indicates "this flight has category-specific data captured." Absence is normal and not an error.
7.2 Initial Subclass Tables to Implement
logbook.helicopter_flight_details— autorotations to touchdown count, hovering time, external load operations.logbook.glider_flight_details— launch method (aerotow / winch / self-launch / bungee), tow aircraft reference, release altitude, max altitude, soaring time.logbook.balloon_flight_details— balloon type (hot air / gas / hybrid), launch site description, landing site description, max altitude.
7.3 Stubs for Future Categories
logbook.lta_flight_details(lighter-than-air — airships and balloons split if needed)logbook.powered_lift_detailslogbook.weight_shift_control_detailslogbook.powered_parachute_details
These can be added as users emerge with the need. The pattern is fixed: PK/FK to flights, category-specific columns.
7.4 Application Logic
The application determines which subclass table(s) to populate based on the flight's aircraft model's category. A flight in a Robinson R44 may have a helicopter_flight_details row. A flight in a glider may have a glider_flight_details row. A flight in a Cessna 172 has no subclass row.
8. Time Modeling
8.1 Hours: NUMERIC(5,1)
All time durations stored in hours, to one decimal place. Range up to 9999.9. Pilot convention is tenths; using NUMERIC avoids floating-point error accumulation across aggregations.
Aggregated totals (career hours) fit in NUMERIC(7,1) easily.
8.2 Moments: timestamptz
All point-in-time events use timestamptz. Aviation crosses time zones routinely; naive timestamps invite corruption.
For the night currency / sunrise-sunset determination, the actual moment of the takeoff or landing is needed in UTC, plus the airport's coordinates, to compute local solar time.
8.3 Block Times
Four optional fields on flights:
block_out— start of movement (pushback from gate, or first taxi)block_off— wheels off (takeoff)block_on— wheels on (landing)block_in— end of movement (arrival at gate, shutdown)
Derived in views, never required:
- block_time = block_in - block_out
- air_time = block_on - block_off
- taxi_out = block_off - block_out
- taxi_in = block_in - block_on
total_time (the 61.51 logged time) is a separate stored column. Application-layer logic may compute and prefill it from block fields, but the user controls it.
8.4 PIC Basis
§61.51(e) defines multiple bases on which PIC time can be logged:
- Sole manipulator of controls when rated for the aircraft category/class/type
- Acting PIC (legal PIC of flight)
- ATP exercising privileges in 14 CFR Part 121 scheduled ops
- Authorized PIC of an operation requiring more than one pilot
A pic_basis_id field on flights references a reference.pic_basis_types lookup table with these values. Default: sole_manipulator. The distinction matters most for SIC currency under §61.55, which asks about sole-manipulator experience specifically.
8.5 Night Definitions
Two different definitions of "night" coexist in 14 CFR:
- §1.1 (logging definition): the time between end of evening civil twilight and beginning of morning civil twilight. Used for
night_timehours on the flight record. - §61.57(b) (currency definition): 1 hour after sunset to 1 hour before sunrise. Used for night takeoff/landing currency.
The schema accommodates both. night_time on flights uses the §1.1 definition. Individual landing rows have is_night_per_6157 computed from the takeoff/landing timestamp and airport location.
A SQL function is_night_per_6157(occurred_at_utc, airport_id) RETURNS boolean implements the §61.57(b) calculation. Similarly is_night_per_part1(occurred_at_utc, airport_id) RETURNS boolean for civil twilight. Both rely on the NOAA solar position algorithm implemented in PLpgSQL or as a pgcrypto/pl/pgsql function.
9. Audit Trail Design
9.1 Structure
A single audit.audit_log table. One row per change to an audited table. Schema documented in section 6.28.
9.2 Trigger Function
A single trigger function audit.audit_trigger_func() is attached to every audited table for INSERT, UPDATE, and DELETE.
Pseudo-logic:
ON event:
determine operation (INSERT/UPDATE/DELETE)
capture row id, table name, timestamp, user
capture old_values (NULL for INSERT) and new_values (NULL for DELETE) as JSONB
read change_reason from session variable (NULL if not set)
read prev_hash from latest audit_log row
compute row_hash = SHA256(prev_hash || canonical_content)
INSERT into audit_log
9.3 Hash Chain
Each audit row contains:
prev_hash—row_hashof the immediately preceding audit_log row (NULL for the very first row)row_hash— SHA-256 of (prev_hash|| canonical serialization of this row's other content)
A verification function walks the audit log from start to finish, recomputing each row's hash and comparing to the stored value. Any tampering — modification, deletion, insertion in the middle — breaks the chain at the tampered point.
The first row in the chain is special; prev_hash is NULL or a defined genesis value, and the chain is anchored there.
9.4 Verification Function
audit.verify_chain(start_id, end_id) RETURNS table(broken_at_id, expected_hash, actual_hash)
Returns rows for any breaks detected. Empty result = chain intact.
Verification can be run periodically (e.g., daily cron job) and the result logged elsewhere, providing a witness that the chain was intact at known times.
9.5 Change Reason
For INSERTs and routine current-row UPDATEs, change_reason is NULL. For UPDATEs and DELETEs to historical or sensitive data, the application sets a PostgreSQL session variable before the operation:
SET LOCAL audit.change_reason = 'Corrected date typo: was 2025-03-14, should be 2025-03-15';
UPDATE logbook.flights SET flight_date = '2025-03-15' WHERE id = ...;
The trigger function reads this variable via current_setting('audit.change_reason', true) and includes it in the audit row.
UI workflows for editing historical or signed data should require the user to provide a reason, which the application then sets as the session variable.
9.6 Audit Table Access Control
The audit log table has its INSERT privilege granted to the application role. UPDATE and DELETE are explicitly revoked from all non-superuser roles. The audit schema's tables are read-only to users (via views or restricted SELECTs) for forensic review.
A separate "audit administrator" role exists at the database level for emergency intervention (e.g., legitimate data correction following discovery of an old bug). Use of this role is itself subject to PostgreSQL's standard logging mechanisms.
10. Entry Locking and Corrections
10.1 Locking Mechanics
The is_locked boolean on flights, training_sessions, endorsements, and book_reports indicates that the entry should not be modified in place.
A trigger on each lockable table:
BEFORE UPDATE:
IF OLD.is_locked = TRUE AND non-trivial-field-changed THEN
RAISE EXCEPTION 'Locked entry cannot be modified. Create a correction.'
END IF
"Non-trivial" excludes the locking metadata itself (is_locked, locked_at, locked_reason can transition; superseded_by_* can be updated by the correction process).
Locks are set by:
- Trigger when an
entry_signaturesrow is created for an entry → set is_locked = TRUE, locked_reason = 'cryptographic_signature'. - User action via the application → locked_reason = 'user_locked'.
- CFI signature recorded in the simple signature fields → locked_reason = 'cfi_signature'.
10.2 Correction Entries
To correct a locked entry, a new entry is created with:
corrects_flight_id(orcorrects_training_session_id) pointing to the original.correction_reasonpopulated with the user's explanation.- All other fields populated with the corrected values.
A trigger then updates the original entry's superseded_by_flight_id to point to the correction (this is the only update permitted on a locked entry's content).
The audit log captures all of this normally.
10.3 Erroneous Entries
If an entry should not have existed at all (e.g., accidental duplicate, fabricated entry discovered), it can be marked is_erroneous = TRUE with erroneous_reason populated. The entry is not deleted; it remains for the audit/historical record.
Display views and aggregation queries filter WHERE is_erroneous = FALSE.
For unlocked entries, hard DELETE is permitted and captured by the audit log. For locked entries, the is_erroneous mechanism is the only deletion path.
10.4 Display Logic
The flat flights view (v_flat_flights) presents the user's logbook as it should be read:
- Entries with
superseded_by_flight_id IS NOT NULLare excluded from the default view (the correction supersedes them). - Entries with
is_erroneous = TRUEare excluded. - A separate
v_flat_flights_completeview includes superseded and erroneous entries with status indicators, for full historical review.
11. Cryptographic Signatures
11.1 Compliance with AC 120-78B
AC 120-78B sets five criteria for valid electronic signatures in FAA recordkeeping:
- Unique identification of the signer
- Signature under signer's sole control
- Intent to sign (deliberate action)
- Linkage to the document (signature invalid if document modified)
- Verifiability
Standard asymmetric cryptographic signatures (PGP/GPG, S/MIME, raw Ed25519) satisfy all five when key management is sound.
11.2 Key Registration
Before someone (typically a CFI) can sign entries in a user's logbook, their public key is registered in logbook.public_keys:
- Linked to a
person_idinlogbook.people. - Fingerprint stored for quick identification.
- Full public key data stored for verification.
- Algorithm and format identified.
- Registration timestamp.
- Optional revocation date if the key is later compromised or replaced.
11.3 Canonical Data Form
To produce a verifiable signature, the bytes signed must be reproducible. Each signable entity has a defined canonicalization:
- Fixed set of fields (defined per
canonical_form_version) - Sorted JSON keys
- UTF-8 encoding, no whitespace
- Specific format for nulls, dates, numerics
The canonical_form_version on each signature row identifies which algorithm was used. New versions can be introduced without invalidating old signatures.
11.4 Signing Workflow
- Pilot creates/finalizes a flight entry.
- Pilot presents entry to CFI for signature (in person, via app, etc.).
- CFI uses their private key (held in their own GPG keyring, smart card, or HSM — never in the logbook system) to sign the canonical representation.
- Application receives the signature and inserts a row in
logbook.entry_signatures:- References the entry
- References the CFI's
person_idandpublic_key_id - Records the canonical form version
- Records the SHA-256 hash of the canonical bytes (for quick verification)
- Records the signature bytes themselves
- Trigger sets
is_locked = TRUEon the entry.
11.5 Verification
A verification function:
- Loads the signature row.
- Reconstructs the canonical bytes from the current entry data (or the historical version via audit log if the entry has been corrected).
- Recomputes the hash and compares to the stored hash.
- Verifies the signature against the public key.
- Returns a result: VALID, HASH_MISMATCH, SIGNATURE_INVALID, KEY_REVOKED, etc.
11.6 Limitations and Caveats
- Private key security is a user responsibility. The system enforces no key management practices on signers.
- Signature validity at a future date depends on the key not being revoked and the canonical form algorithm being preserved.
- For long-term archival (decades), additional measures (timestamping services, periodic re-signing) may be needed. Out of scope for v1.
12. Lookup Tables and Enums
12.1 Lookup Tables
Used for category-like data with metadata. All lookup tables in reference schema, shared across users, follow a standard structure:
id(UUID v7, PK)code(text, unique) — short stable identifier used in queries and exportsname(text) — display namedescription(text)display_order(integer)is_deprecated(boolean, default false)effective_from(date, nullable) — for time-varying definitionseffective_to(date, nullable)created_at,updated_at(timestamptz)
12.2 Effective-Dated Lookups
Some reference data has changed over time (training device level definitions, regulatory categories). Effective dates let the lookup table represent the regulatory history:
- "FTD Level X (1995-2010)" —
effective_to = '2010-XX-XX' - "FTD Level X (2010-present)" —
effective_from = '2010-XX-XX'
Entries from 2008 reference the old definition; entries from 2020 reference the new. Both definitions remain accessible.
12.3 Lookup Tables to Define
aircraft_categories— airplane, rotorcraft, glider, lighter-than-air, powered-lift, weight-shift-control, powered-parachuteaircraft_classes— single-engine land, multi-engine land, single-engine sea, multi-engine sea, helicopter, gyroplane, balloon, airshipapproach_types— ILS, LOC, LOC-BC, VOR, NDB, RNAV-GPS-LNAV, RNAV-GPS-LPV, RNAV-GPS-LNAV/VNAV, GLS, PAR, ASR, ILS-CAT-II, ILS-CAT-III, visual, contact, etc.certificate_levels— Student, Sport, Recreational, Private, Commercial, ATP, Flight Instructor, Ground Instructor, Remote Pilot, Mechanic, Repairman, etc.rating_types— Instrument-Airplane, Multi-Engine Land, Single-Engine Sea, Type ratings (with type code), CFI category/class, Sport pilot endorsements, etc.endorsement_types— high_performance, complex, tailwheel, high_altitude, pressurized, type-specific, solo, solo_cross_country, solo_90day_repeat, flight_review, IPC, BFR, etc.medical_classes— Class 1, Class 2, Class 3, BasicMed, Special Issuancerole_on_flight_types— PIC, SIC, CFI_giving, student_receiving, safety_pilot, flight_attendant, jumpseat_observer, relief_pilot, IRO, check_airman, examiner, etc.training_device_types— FFS, FTD, ATD, BATD, AATD, unknown_historicalpic_basis_types— sole_manipulator, acting_pic, atp_part_121, more_than_one_pilot_requiredtraining_event_types— recurrent_training, AQP, CQT, type_rating_initial, type_rating_recurrent, §61.55_familiarization, ground_school, OE_initial, line_check, etc.role_qualification_types— forperson_roles: CFI, CFII, MEI, ATP, Commercial, Private, flight_attendant, dispatcher, A&P_mechanic, IA, DPE, ACR, FAA_inspector, etc.far_citations— regulation reference table for cross-linking
12.4 Enums (Native PostgreSQL ENUM Types)
Reserved for small, genuinely fixed bounded sets that don't need metadata:
audit_operation— 'INSERT', 'UPDATE', 'DELETE'practical_test_outcome— 'pass', 'fail', 'discontinuance', 'notice_of_disapproval'training_event_status— 'planned', 'scheduled', 'in_progress', 'completed', 'cancelled'landing_full_stop_type— 'full_stop', 'touch_and_go', 'stop_and_go'
These are unlikely to change. Adding values is a simple ALTER TYPE in modern PostgreSQL.
13. Views
13.1 v_flat_flights
Denormalized flat view of flights, one row per flight (or one per leg, with a parallel v_flat_flight_legs). All joins pre-resolved. The primary interface for the query builder and exports.
Fields include: flight_id, leg_id, date, owner_user_id, aircraft tail and model, all time categories, all derived flags (is_complex, is_high_performance, etc.), origin and destination airports, approach counts, landings counts, instructor name, free-text remarks, signature status, lock status.
Excludes superseded and erroneous entries by default.
13.2 v_flat_flights_complete
Same as v_flat_flights but includes superseded and erroneous entries, with explicit status columns. For audit review and full historical display.
13.3 v_flights_chronological
Flights only, ordered by date. The "scroll through my logbook" view.
13.4 v_events_chronological
Non-flight events only — endorsements, medicals, flight reviews, practical tests, training events, certificates issued — ordered by date. The view that lets you find "when did I last get a medical?" without scrolling past hundreds of flights.
13.5 v_logbook_chronological
Union of flights and events, ordered by date. The combined view.
13.6 v_currency_status_live
Real-time currency calculation. For each pilot and each relevant currency window (90-day passenger day, 90-day passenger night, IFR 6-month, flight review 24-month, medical class-specific):
- Current status (current, expired, in grace period, requires IPC, etc.)
- Date of last qualifying event
- Date currency expires (or expired)
- Days remaining (negative if expired)
13.7 v_expirations_upcoming
All expirable items (medicals, flight reviews, currency windows, endorsements with expiration dates) with their expiration dates and days remaining. Filterable by days-out for "what expires in the next 90 days" displays.
13.8 v_aeronautical_experience_totals
Career totals across the standard 61.51 categories plus subcategories needed for 61.65, 61.129, 61.159 evaluations. Suitable for ATP qualification queries.
13.9 v_aircraft_summary
Per-aircraft time totals — useful for "how many hours have I flown in this airplane."
13.10 v_audit_log_readable
Joins audit_log with users and humans-friendly formatting for forensic review.
14. Book Reports
14.1 Generation
A function generate_book_report(user_id, as_of_date, report_type) runs the appropriate queries against the live data as of the as-of date and inserts a row in logbook.book_reports capturing the result.
report_data is JSONB containing whatever the report type defines. Example for report_type = 'pre_flight_check':
{
"as_of_date": "2026-05-16",
"medical_status": { "class": 1, "expires": "2026-08-31", "days_remaining": 107 },
"flight_review": { "last_completed": "2024-08-15", "expires": "2026-08-31", "status": "current" },
"currency_passenger_day": { "status": "current", "expires": "2026-07-04" },
"currency_passenger_night": { "status": "expired", "expired_on": "2026-04-12" },
"currency_ifr": { "status": "current", "approaches_logged": 6, "expires": "2026-09-30" },
"endorsements_current": [...],
"upcoming_expirations": [...]
}
14.2 Attestation
For reports intended for external presentation, the user invokes attestation via the application. The application:
- Displays the report content one more time.
- Presents the attestation statement (configurable text, e.g., "I certify the information in this report is true and correct to the best of my knowledge.")
- Requires explicit confirmation.
- Updates the report row:
is_attested = TRUE,attested_at = now(),attested_by_person_id = (logbook owner),attestation_text = (the statement). - Optionally invokes cryptographic signing — creates an
entry_signaturesrow referencing the book report.
The attestation is itself audited via the audit log.
14.3 Reports as Historical Records
Once generated, reports are immutable (the same locking principle as flights). They become part of the historical record — "what did my logbook say on 2026-05-16?" is answered by retrieving the report generated that day.
Reports can be exported as PDF for paper retention.
15. Constraints and Indexes
15.1 Check Constraints
Sanity constraints enforced at the database level:
- All time columns >= 0
- All time subcategories <= total_time + 0.1 (rounding allowance)
- Date sanity:
flight_date >= '1903-12-17'(Wright Brothers floor) - Block time ordering where multiple block fields present
(corrects_flight_id IS NULL) OR (correction_reason IS NOT NULL)(is_erroneous = FALSE) OR (erroneous_reason IS NOT NULL)- Locked entries can only have lock-related fields updated
- Exactly-one constraints for XOR foreign keys (e.g., approach belongs to either a flight leg or a training session)
15.2 Partial Indexes
Optimized for common query patterns:
flightsrecent flights index:ON (owner_user_id, flight_date DESC) WHERE flight_date >= CURRENT_DATE - INTERVAL '24 months'landings_takeoffsfor night currency:ON (owner_user_id, occurred_at DESC) WHERE is_landing = TRUE AND is_full_stop = TRUEapproachesfor IFR currency:ON (owner_user_id, flight_leg_id, training_session_id) WHERE was_missed = FALSE OR to_landing = TRUE- Active records: many tables have
WHERE is_erroneous = FALSE AND superseded_by_* IS NULLpartial indexes
15.3 Standard Indexes
- All foreign keys indexed
- All natural unique keys indexed (registration, certificate_number, etc.)
- Owner_user_id appears in most multi-column indexes (RLS optimization)
15.4 Row-Level Security
Every logbook schema table has RLS enabled with a policy:
CREATE POLICY user_isolation ON logbook.xxx
USING (owner_user_id = current_setting('app.current_user_id')::uuid);
The application sets app.current_user_id per session/transaction. Reference schema tables have no RLS (shared read-only).
16. Import / Export
16.1 Open JSON Format
The canonical export format is JSON. A documented, versioned schema. Includes:
- Schema version
- Export metadata (timestamp, source system, user identifier — opaque, not the email)
- All flights with legs, landings, approaches
- All training sessions
- All people and person_roles
- All certificates, ratings, endorsements, medicals
- All flight reviews, practical tests, training events
- All tags and tag assignments
- All public keys and signatures (signatures verifiable post-import)
- Audit log
- Category-specific subclass table data
- References to (but not contents of) attachments — attachment files exported separately as a tarball or ZIP
UUIDs are preserved across export/import. Re-importing into the same instance is a no-op (no duplicates); importing into a fresh instance reconstitutes the logbook.
16.2 Format Versioning
schema_version: "1.0" field at the top of every export. Reader code is version-aware and can read older versions.
16.3 CSV Compatibility
Two CSV import/export options for migration to/from major proprietary apps:
- MyFlightbook CSV format
- ForeFlight CSV format
These are lossy — they don't carry audit history, signatures, or full schema — but enable users to migrate in or out.
16.4 PDF Export
For §61.51(i) inspection: a printable PDF of the logbook in traditional layout, generated from v_flat_flights and related views. Application-layer functionality, not schema. Schema supports it by providing complete and consistent data.
16.5 Open Standard Publication
The JSON schema specification will be published separately (e.g., GitHub repository, CC0 license) to encourage adoption by other logbook software. A reference name (e.g., "Open Pilot Logbook Format" / OPLF) will be chosen.
17. Migrations
17.1 Tool: Flyway
Plain SQL migration files in a versioned directory:
migrations/
V001__initial_reference_schema.sql
V002__lookup_tables.sql
V003__core_entities.sql
V004__flights_and_legs.sql
V005__landings_takeoffs.sql
V006__approaches.sql
V007__training_sessions.sql
V008__certificates_ratings_endorsements.sql
V009__medicals_reviews_tests.sql
V010__training_events.sql
V011__attachments_tags.sql
V012__signatures_and_keys.sql
V013__book_reports.sql
V014__audit_log.sql
V015__audit_triggers.sql
V016__views.sql
V017__rls_policies.sql
V018__seed_reference_data.sql
V019__functions_sunrise_sunset.sql
V020__functions_currency.sql
...
17.2 Conventions
- Forward-only. Mistakes get a new forward migration, not a rollback.
- Each migration is transactional where PostgreSQL allows.
- Reference data seeds (lookup tables, well-known airports) live in dedicated migrations.
- Schema changes precede data migrations.
- Comments in migrations document the regulatory or design rationale where relevant.
17.3 Docker Integration
Flyway's official Docker image runs as a one-shot container during deployment. Migrations apply automatically on database initialization or service deployment.
18. Backup Strategy
Operational concern; mentioned here for schema-related considerations:
- Logical backup: nightly
pg_dumparchived off-site. Recoverable across PostgreSQL versions. - Physical backup with WAL archiving: continuous WAL streaming for point-in-time recovery within a recent window.
- Attachment backup: object storage (or filesystem) backed up separately, on a coordinated schedule.
- Audit log priority: verify that the audit log is included in every backup; verify periodically that restored audit logs validate their hash chains.
- Backup testing: schedule periodic restoration tests. An unverified backup is no backup.
UUIDs in the schema ensure that backups can be restored to alternative instances without identifier collisions.
19. Out of Scope (Explicit)
The following are not addressed by this schema. They may be addressed by separate systems, possibly sharing this database server but not this schema:
- Duty and rest time tracking (Part 117, Part 91/135 duty rules) — belongs in scheduling software.
- Aircraft maintenance tracking (next inspection due, AD compliance, component time tracking) — belongs in maintenance software.
- Flight scheduling — assignments, crew pairings, dispatch.
- Weather data and integrations.
- Flight planning — routes, fuel calculations, performance.
- Real-time flight tracking — ADS-B integration.
- Cost/expense tracking — fuel costs, rental costs, training expenses.
- Insurance documentation — though
book_reportscan produce documentation for insurance use. - Currency-action workflow (e.g., automatically scheduling a refresher when currency lapses).
20. Future Considerations / Open Questions
Items deferred for later resolution:
- Query builder UI: how to implement safe user-defined queries. Likely a point-and-click builder over
v_flat_flightsand friends. - WINGS program integration: automatic credit detection from flight log.
- PRD (Pilot Records Database) reporting: the §44703 mandate for record-sharing between airlines.
- Multi-key signatures: a single entry signed by multiple parties (e.g., student and CFI both sign).
- Long-term signature validity: integration with RFC 3161 timestamping services or similar.
- Aircraft and people deduplication: if multi-user with sharing is ever revisited, mechanisms for users to optionally point at shared records.
- Internationalization: non-FAA aviation regulatory environments (EASA, Transport Canada, etc.). The schema is largely category-neutral but currency/certificate logic is currently FAA-specific.
- Offline-first mobile app: synchronization model if a mobile companion app is built.
21. Glossary
- §61.51 — 14 CFR 61.51, Pilot Logbooks. The baseline regulation.
- ACS / PTS — Airman Certification Standards / Practical Test Standards. Examination criteria for practical tests.
- AME — Aviation Medical Examiner.
- ATD — Aviation Training Device. A category of training device less capable than an FTD.
- AATD — Advanced Aviation Training Device. Higher-fidelity ATD.
- BATD — Basic Aviation Training Device. Lower-fidelity ATD.
- BasicMed — A medical qualification alternative to traditional medical certificates, established by FAA reform circa 2017.
- CFI / CFII / MEI — Certified Flight Instructor / Instrument-Instructor / Multi-Engine Instructor.
- DPE — Designated Pilot Examiner.
- FFS — Full Flight Simulator. Highest-fidelity training device, certified to levels A-D.
- FTD — Flight Training Device. Mid-fidelity training device.
- IPC — Instrument Proficiency Check. Required to regain IFR currency after lapse beyond grace period.
- MGTOW — Maximum Gross Takeoff Weight.
- PIC / SIC — Pilot in Command / Second in Command.
- TAA — Technically Advanced Aircraft. Per §61.129, an airplane with specific avionics.
- UUID v7 — Time-ordered universally unique identifier.
22. Appendix A — 14 CFR 61.51 Requirements Mapping
How each §61.51(b)-(j) requirement is satisfied by the schema:
| Requirement | Source | Schema Element |
|---|---|---|
| Date | §61.51(b)(1)(i) | flights.flight_date, training_sessions.session_date |
| Total flight or lesson time | §61.51(b)(1)(ii) | flights.total_time, training_sessions.total_time |
| Location of departure and arrival | §61.51(b)(1)(iii) | flight_legs.from_airport_id, to_airport_id |
| Type and identification of aircraft / device | §61.51(b)(1)(iv) | flights.aircraft_id → model; training_sessions.training_device_id → model |
| Solo time | §61.51(b)(2)(i) | flights.solo_time |
| PIC time | §61.51(b)(2)(ii) | flights.pic_time |
| SIC time | §61.51(b)(2)(iii) | flights.sic_time |
| Flight instruction received | §61.51(b)(2)(iv) | flights.dual_received, training_sessions.dual_received |
| Training received in FFS/FTD/ATD | §61.51(b)(2)(v) | training_sessions.dual_received |
| Day / night | §61.51(b)(3)(i) | flights.night_time; landings/takeoffs is_day_per_part1, is_night_per_6157 |
| Actual instrument | §61.51(b)(3)(ii) | flights.actual_instrument |
| Simulated instrument | §61.51(b)(3)(iii) | flights.simulated_instrument, training_sessions.instrument_time_simulated |
| Instrument approaches | §61.51(g)(1) | logbook.approaches |
| Safety pilot name | §61.51(c)(1) | logbook.flight_crew with role safety_pilot |
| Instruction given | §61.51(h) | flights.dual_given, training_sessions.dual_given |
| Instructor signature | §61.51(j) | flights.cfi_signature_*, logbook.entry_signatures |
23. Appendix B — Regulatory Reference Index
- 14 CFR 1.1 — Definitions; night (logging), cross-country
- 14 CFR 61.1 — Definitions; cross-country variants for ratings
- 14 CFR 61.31 — Type rating, complex, high-performance, tailwheel, high-altitude, pressurized
- 14 CFR 61.39 — Prerequisites for practical tests
- 14 CFR 61.51 — Pilot logbooks (the floor)
- 14 CFR 61.55 — SIC qualifications
- 14 CFR 61.56 — Flight review
- 14 CFR 61.57 — Recent flight experience (currency)
- 14 CFR 61.65 — Instrument rating
- 14 CFR 61.129 — Commercial pilot
- 14 CFR 61.159 — ATP (1500-hour rule)
- AC 120-78B — Electronic signatures and recordkeeping
- FAA Order 8900.1 — FSIMS
24. Appendix C — High-Level Entity Relationship Diagram
erDiagram
USERS ||--o{ PEOPLE : owns
USERS ||--o{ AIRCRAFT : owns
USERS ||--o{ TRAINING_DEVICES : owns
USERS ||--o{ FLIGHTS : owns
USERS ||--o{ TRAINING_SESSIONS : owns
PEOPLE ||--o{ PERSON_ROLES : has
PEOPLE ||--o{ FLIGHT_CREW : appears_in
PEOPLE ||--o{ PUBLIC_KEYS : registers
PEOPLE ||--o{ ENDORSEMENTS : endorses
PEOPLE ||--o{ ENTRY_SIGNATURES : signs
AIRCRAFT_MODELS ||--o{ AIRCRAFT : "instantiated as"
TRAINING_DEVICE_MODELS ||--o{ TRAINING_DEVICES : "instantiated as"
AIRCRAFT ||--o{ FLIGHTS : "flown in"
TRAINING_DEVICES ||--o{ TRAINING_SESSIONS : "trained in"
FLIGHTS ||--o{ FLIGHT_LEGS : contains
FLIGHTS ||--o{ FLIGHT_CREW : "has crew"
FLIGHT_LEGS ||--o{ LANDINGS_TAKEOFFS : "has events"
FLIGHT_LEGS ||--o{ APPROACHES : "has approaches"
TRAINING_SESSIONS ||--o{ FLIGHT_CREW : "has crew"
TRAINING_SESSIONS ||--o{ APPROACHES : "has approaches"
AIRPORTS ||--o{ FLIGHT_LEGS : "departure/arrival"
AIRPORTS ||--o{ LANDINGS_TAKEOFFS : "location"
AIRPORTS ||--o{ APPROACHES : "location"
USERS ||--o{ CERTIFICATES : holds
CERTIFICATES ||--o{ RATINGS : "has ratings"
CERTIFICATES ||--o{ CERTIFICATE_LIMITATIONS : "has limitations"
USERS ||--o{ ENDORSEMENTS : received
USERS ||--o{ MEDICALS : holds
USERS ||--o{ FLIGHT_REVIEWS : completed
USERS ||--o{ PRACTICAL_TESTS : took
USERS ||--o{ TRAINING_EVENTS : participated_in
USERS ||--o{ BOOK_REPORTS : generated
USERS ||--o{ ATTACHMENTS : "uploaded"
USERS ||--o{ TAGS : "defined"
FLIGHTS ||--o{ ENTRY_SIGNATURES : "may be signed"
TRAINING_SESSIONS ||--o{ ENTRY_SIGNATURES : "may be signed"
BOOK_REPORTS ||--o{ ENTRY_SIGNATURES : "may be signed"
PUBLIC_KEYS ||--o{ ENTRY_SIGNATURES : verifies
This is a simplified high-level ERD. Detailed entity-by-entity diagrams will accompany the DDL phase.
End of design document v0.1.
Comments, additions, and revisions to feed back into v0.2 before DDL generation:
- (placeholder for review notes)