tabulae-aeriae/architecture-overview.md
2026-05-02 21:24:40 -04:00

21 KiB

Pilot Logbook — Architecture Overview

Project Summary

A self-hostable, web-based pilot logbook system. FAA-compliant (14 CFR 61.51) by default, with optional EASA compliance as a configurable profile. Designed for low-resource deployment (Raspberry Pi / Docker Swarm). PostgreSQL backend, Go API server, lightweight web frontend.

Licensing model: Free open-source tier for non-commercial pilots (Part 91, recreational, student). Paid tier for professional pilots (Part 135, 121) with crew management, duty/rest calculations, and advanced compliance features.


High-Level Architecture

┌─────────────────────────────────────────────────┐
│                   Browser                       │
│          (React + Tailwind or HTMX)             │
└──────────────────────┬──────────────────────────┘
                       │ HTTPS (JSON API)
                       ▼
┌─────────────────────────────────────────────────┐
│                Go API Server                    │
│                                                 │
│  ┌───────────┐ ┌──────────┐ ┌───────────────┐  │
│  │ Auth/RBAC │ │ Logbook  │ │  Reporting    │  │
│  │ Middleware │ │ Service  │ │  Engine       │  │
│  └───────────┘ └──────────┘ └───────────────┘  │
│  ┌───────────┐ ┌──────────┐ ┌───────────────┐  │
│  │ Aircraft  │ │ Airport  │ │  Audit Trail  │  │
│  │ Service   │ │ Service  │ │  Service      │  │
│  └───────────┘ └──────────┘ └───────────────┘  │
│  ┌───────────┐ ┌──────────┐                     │
│  │ Currency  │ │ Medical  │                     │
│  │ Tracker   │ │ Reporter │                     │
│  └───────────┘ └──────────┘                     │
│                                                 │
│         DB Interface (database/sql)             │
└──────────────────────┬──────────────────────────┘
                       │
                       ▼
              ┌─────────────────┐
              │   PostgreSQL    │
              │                 │
              │  Audit triggers │
              │  installed on   │
              │  tracked tables │
              └─────────────────┘

Key Design Decisions

  • Go standard library router (net/http) or Chi — Chi is minimal and well-proven. No heavy frameworks. Keeps binary small.
  • database/sql with pgx driver — Direct SQL, no ORM. Migrations handled by golang-migrate or a simple embedded migration system.
  • Audit trail via PostgreSQL triggers — Changes to logbook entries, aircraft, and user data are captured automatically at the DB level. The application doesn't need to remember to log; the trigger handles it.
  • Frontend TBD — Two reasonable paths: (1) React + Tailwind (like jetlog) for a modern SPA, or (2) Go templates + HTMX for a server-rendered approach that eliminates the Node.js build step entirely. The HTMX path is simpler to deploy, lighter, and arguably better suited to a Pi. Worth discussing.

Database Schema (Core Tables)

Users & Auth

CREATE TABLE users (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username        VARCHAR(100) UNIQUE NOT NULL,
    email           VARCHAR(255) UNIQUE,
    password_hash   TEXT NOT NULL,
    role            VARCHAR(20) NOT NULL DEFAULT 'pilot',  -- pilot, admin
    pilot_cert_num  VARCHAR(50),          -- FAA certificate number
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

Aircraft

CREATE TABLE aircraft (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id),
    tail_number     VARCHAR(20) NOT NULL,
    type_code       VARCHAR(10),          -- ICAO type designator (e.g., C172)
    make_model      VARCHAR(100) NOT NULL, -- Cessna 172S Skyhawk SP
    category_class  VARCHAR(50) NOT NULL,  -- ASEL, AMEL, RH, Glider, etc.
    is_complex      BOOLEAN DEFAULT FALSE,
    is_high_perf    BOOLEAN DEFAULT FALSE,
    is_tailwheel    BOOLEAN DEFAULT FALSE,
    is_taa          BOOLEAN DEFAULT FALSE, -- Technologically Advanced Aircraft
    gear_type       VARCHAR(20),           -- fixed_tri, retract, conventional, float, etc.
    engine_type     VARCHAR(20),           -- piston, turboprop, jet, electric
    num_engines     SMALLINT DEFAULT 1,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(user_id, tail_number)
);

Airports

-- Seeded from OurAirports or similar open dataset
-- Users can also add custom/private strips
CREATE TABLE airports (
    id              SERIAL PRIMARY KEY,
    icao_code       VARCHAR(4),
    faa_code        VARCHAR(4),           -- FAA LID (e.g., JFK, 7B2)
    iata_code       VARCHAR(3),
    name            VARCHAR(200) NOT NULL,
    city            VARCHAR(100),
    state_region    VARCHAR(100),
    country         VARCHAR(2),
    latitude        DECIMAL(10, 7),
    longitude       DECIMAL(10, 7),
    elevation_ft    INTEGER,
    type            VARCHAR(30),          -- large_airport, medium_airport, small_airport, heliport, seaplane_base, closed
    is_user_defined BOOLEAN DEFAULT FALSE,
    user_id         UUID REFERENCES users(id),  -- NULL for system airports
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_airports_icao ON airports(icao_code);
CREATE INDEX idx_airports_faa ON airports(faa_code);

Flight Log Entries (the core table)

This is the big one. Columns are driven by FAA 14 CFR 61.51 requirements plus common practical fields.

CREATE TABLE flights (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id             UUID NOT NULL REFERENCES users(id),

    -- Date & Route
    flight_date         DATE NOT NULL,
    departure_airport   VARCHAR(10) NOT NULL, -- ICAO or FAA LID
    arrival_airport     VARCHAR(10) NOT NULL,
    route               TEXT,                  -- free-text route of flight

    -- Aircraft
    aircraft_id         UUID REFERENCES aircraft(id),
    tail_number         VARCHAR(20) NOT NULL,  -- denormalized for historical accuracy

    -- Times (stored in minutes for easy math; displayed as HH:MM)
    total_time          INTEGER NOT NULL DEFAULT 0,   -- total flight time
    pic_time            INTEGER DEFAULT 0,
    sic_time            INTEGER DEFAULT 0,
    solo_time           INTEGER DEFAULT 0,
    dual_received       INTEGER DEFAULT 0,
    dual_given          INTEGER DEFAULT 0,   -- CFI logging instruction given
    cross_country       INTEGER DEFAULT 0,
    night_time          INTEGER DEFAULT 0,

    -- Instrument
    actual_instrument   INTEGER DEFAULT 0,
    simulated_instrument INTEGER DEFAULT 0,
    holds               SMALLINT DEFAULT 0,

    -- Instrument Approaches (stored as JSONB for flexibility)
    -- e.g., [{"type": "ILS", "airport": "KTPA", "runway": "19L"}, ...]
    approaches          JSONB DEFAULT '[]',

    -- Landings
    day_landings        SMALLINT DEFAULT 0,
    night_landings      SMALLINT DEFAULT 0,
    day_full_stop       SMALLINT DEFAULT 0,   -- needed for tailwheel currency
    night_full_stop     SMALLINT DEFAULT 0,   -- needed for night currency

    -- Simulator / Training Device
    simulator_time      INTEGER DEFAULT 0,
    sim_type            VARCHAR(20),          -- FTD, AATD, BATD, FFS
    
    -- Ground Training
    ground_training     INTEGER DEFAULT 0,

    -- People
    persons_on_board    TEXT,                  -- free text or JSONB
    instructor_name     VARCHAR(100),
    instructor_cert_num VARCHAR(50),

    -- Remarks & Endorsements
    remarks             TEXT,

    -- Metadata
    is_checkride        BOOLEAN DEFAULT FALSE,
    is_ipc              BOOLEAN DEFAULT FALSE, -- instrument proficiency check
    is_bfr              BOOLEAN DEFAULT FALSE, -- biennial flight review (now "flight review")
    flight_review       BOOLEAN DEFAULT FALSE, -- alias for BFR per 61.56

    created_at          TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_flights_user_date ON flights(user_id, flight_date DESC);
CREATE INDEX idx_flights_user_aircraft ON flights(user_id, aircraft_id);

Audit Log

CREATE TABLE audit_log (
    id              BIGSERIAL PRIMARY KEY,
    table_name      VARCHAR(50) NOT NULL,
    record_id       UUID NOT NULL,
    action          VARCHAR(10) NOT NULL,    -- INSERT, UPDATE, DELETE
    field_name      VARCHAR(100),            -- NULL for INSERT/DELETE
    old_value       TEXT,
    new_value       TEXT,
    changed_by      UUID REFERENCES users(id),
    changed_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_audit_record ON audit_log(table_name, record_id);
CREATE INDEX idx_audit_time ON audit_log(changed_at);

-- Trigger function (simplified; real version would iterate over changed columns)
CREATE OR REPLACE FUNCTION audit_trigger_fn()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO audit_log(table_name, record_id, action, new_value, changed_at)
        VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', row_to_json(NEW)::TEXT, now());
        RETURN NEW;
    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO audit_log(table_name, record_id, action, old_value, new_value, changed_at)
        VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', row_to_json(OLD)::TEXT, row_to_json(NEW)::TEXT, now());
        RETURN NEW;
    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO audit_log(table_name, record_id, action, old_value, changed_at)
        VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', row_to_json(OLD)::TEXT, now());
        RETURN OLD;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- Attach to flights table
CREATE TRIGGER flights_audit
    AFTER INSERT OR UPDATE OR DELETE ON flights
    FOR EACH ROW EXECUTE FUNCTION audit_trigger_fn();

Saved Reports

CREATE TABLE saved_reports (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id),
    name            VARCHAR(100) NOT NULL,       -- "8710 Application", "Insurance Renewal", "Annual Totals"
    description     TEXT,
    report_type     VARCHAR(30) NOT NULL,         -- summary, detailed, 8710, medical, currency, custom
    config          JSONB NOT NULL,               -- filters, date ranges, grouping, columns to include
    is_quick_access BOOLEAN DEFAULT FALSE,        -- pin to dashboard for one-click access
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

Currency Tracking

-- Predefine standard FAA currency rules; users can add custom ones
CREATE TABLE currency_rules (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID REFERENCES users(id),    -- NULL = system-defined
    name            VARCHAR(100) NOT NULL,         -- "Night Currency (61.57a)", "IFR Currency (61.57c)"
    regulation_ref  VARCHAR(50),                   -- "61.57(a)", "61.57(c)", etc.
    rule_config     JSONB NOT NULL,                -- defines the lookback window, required counts, etc.
    is_active       BOOLEAN DEFAULT TRUE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Example rule_config for night passenger currency:
-- {
--   "lookback_days": 90,
--   "required": {
--     "night_full_stop": 3
--   },
--   "applies_to": {
--     "category_class": ["ASEL", "AMEL"]
--   }
-- }

Medical Certificates (for the one-click medical report)

CREATE TABLE medical_certificates (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id),
    class           SMALLINT NOT NULL,            -- 1, 2, 3
    issue_date      DATE NOT NULL,
    expiry_date     DATE NOT NULL,                -- calculated per 61.23 based on age + class
    examiner_name   VARCHAR(100),
    notes           TEXT,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

Reporting Engine Design

The goal is to make reporting maximally flexible without requiring users to write SQL.

Built-in Report Types

Report Description Key Fields
8710 FAA Airman Certificate application totals Total time, PIC, SIC, XC, night, instrument (actual/sim), category/class breakdowns
Medical Total time + last 6 months One-click: total all-time hours, total hours in last 6 calendar months
Currency Current status of all active currency rules Green/yellow/red status per rule, days remaining, what's needed to regain currency
Insurance Configurable — typically total time, time in type, retract time, last 90 days User defines which fields matter
Period Summary Totals for any date range All time categories summed for the range
By Aircraft Breakdown by tail number or type Time per aircraft/type with sub-totals

Custom Report Builder

Users can create a saved report by selecting:

  1. Date range — fixed dates, rolling window (last N days/months), or all-time
  2. Filters — by aircraft, category/class, airport, tags
  3. Grouping — by month, year, aircraft type, category/class
  4. Columns — which time fields to include
  5. Output format — on-screen table, PDF, CSV

The saved_reports.config JSONB column stores all of this, so reconstructing the report is just loading the config and re-running the query.

One-Click Reports

Certain reports (8710 totals, medical totals, currency status) are flagged as is_quick_access = true and rendered as dashboard widgets or a single button press. The medical report specifically:

  • Total time (all time) — sum of total_time across all flights
  • Last 6 calendar months — sum of total_time where flight_date >= first day of (current month - 6)

No configuration needed, no date pickers. One click, two numbers.


API Structure (Go)

/api/v1/
├── auth/
│   ├── POST   /login
│   ├── POST   /logout
│   └── POST   /refresh
│
├── flights/
│   ├── GET    /                    -- list (paginated, filterable)
│   ├── POST   /                    -- create
│   ├── GET    /:id                 -- get one
│   ├── PUT    /:id                 -- update
│   ├── DELETE /:id                 -- delete
│   └── POST   /import              -- CSV/bulk import
│
├── aircraft/
│   ├── GET    /
│   ├── POST   /
│   ├── GET    /:id
│   ├── PUT    /:id
│   └── DELETE /:id
│
├── airports/
│   ├── GET    /                    -- search/autocomplete
│   ├── GET    /:code               -- lookup by ICAO or FAA LID
│   └── POST   /                    -- add custom airport
│
├── reports/
│   ├── GET    /8710                -- 8710 totals
│   ├── GET    /medical             -- medical totals (one-click)
│   ├── GET    /currency            -- all currency statuses
│   ├── GET    /summary?from=&to=   -- period summary
│   ├── POST   /custom              -- run a custom report from config
│   ├── GET    /saved               -- list saved reports
│   ├── POST   /saved               -- save a report config
│   ├── PUT    /saved/:id           -- update saved report
│   └── DELETE /saved/:id           -- delete saved report
│
├── audit/
│   ├── GET    /                    -- paginated audit log
│   └── GET    /record/:table/:id   -- audit history for a specific record
│
├── medical/
│   ├── GET    /
│   ├── POST   /
│   └── GET    /status              -- current medical validity
│
└── export/
    ├── GET    /csv                  -- full logbook CSV export
    └── GET    /pdf                  -- PDF export (FAA format)

Go Project Layout

/
├── cmd/
│   └── server/
│       └── main.go                 -- entry point
├── internal/
│   ├── config/                     -- env var parsing, app config
│   ├── database/
│   │   ├── migrations/             -- SQL migration files
│   │   ├── postgres.go             -- connection setup
│   │   └── migrate.go              -- migration runner
│   ├── models/                     -- struct definitions
│   │   ├── flight.go
│   │   ├── aircraft.go
│   │   ├── airport.go
│   │   ├── user.go
│   │   └── report.go
│   ├── handlers/                   -- HTTP handlers (one file per resource)
│   │   ├── flights.go
│   │   ├── aircraft.go
│   │   ├── reports.go
│   │   └── auth.go
│   ├── services/                   -- business logic
│   │   ├── flights.go
│   │   ├── currency.go
│   │   ├── reporting.go
│   │   └── audit.go
│   ├── middleware/
│   │   ├── auth.go
│   │   ├── logging.go
│   │   └── cors.go
│   └── seed/                       -- airport data seeding
│       └── airports.go
├── web/                            -- frontend assets (if using HTMX/templates)
│   ├── templates/
│   └── static/
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── go.sum

Deployment (Docker Compose for Swarm)

version: "3.8"

services:
  logbook:
    image: yourproject/logbook:latest
    ports:
      - "8080:8080"
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: logbook
      DB_USER: logbook
      DB_PASSWORD_FILE: /run/secrets/db_password
      JWT_SECRET_FILE: /run/secrets/jwt_secret
      LOG_LEVEL: info
    secrets:
      - db_password
      - jwt_secret
    deploy:
      replicas: 1
      resources:
        limits:
          memory: 128M    # Go binary + serving requests
          cpus: "0.5"
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: logbook
      POSTGRES_USER: logbook
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.5"

volumes:
  pgdata:

secrets:
  db_password:
    external: true
  jwt_secret:
    external: true

Estimated total memory footprint: ~200-300MB for both containers on a Pi. The Go binary itself will likely be 15-30MB and use ~20-50MB of RAM under normal load.


Open Questions / Next Steps

  1. Frontend approach — React SPA vs. Go templates + HTMX? HTMX eliminates the Node build pipeline entirely, keeps the Docker image smaller, and is arguably simpler. React gives richer interactivity for things like the report builder and map visualizations. A hybrid is possible (HTMX for most pages, a small JS bundle for the map/charts).

  2. Project name — Needed before creating the repo.

  3. Map/visualization library — OpenLayers (heavier, more capable) vs. Leaflet (lighter) vs. something React-based like react-simple-maps. Depends on frontend choice.

  4. EASA profile — How deep to go in v1? Could be as simple as alternative column labels and a different PDF export layout, or a full parallel schema with EASA-specific fields (multi-pilot time, SPSE/SPME columns, etc.).

  5. Import formats — ForeFlight CSV is probably the most common for FAA pilots. LogTen Pro is another big one. Both have documented export formats.

  6. Authentication — JWT for API, but also support OAuth2 (Google, etc.) for the hosted/multi-user scenario? Or just username/password for v1?

  7. License choice — Apache 2.0, MIT, or AGPL? AGPL would prevent someone from running a competing hosted service without open-sourcing their changes, which aligns with the free/paid tier model.