System Architecture

Hive is built around one central idea: ModelDefinition is the shared contract. Every layer — HTTP routing, persistence, validation, trigger pipeline, caching, and frontend rendering — derives its behaviour from a single ModelDefinition struct declared once per entity.

One definition drives the entire stack

Traditional CRUD applications duplicate schema intent in at least five places: route declarations, DTO validation, ORM mappings, business-layer constraints, and frontend form/table definitions. Divergence is the inevitable result. Hive eliminates this by treating ModelDefinition as the canonical source of truth for every layer.

ModelDefinition                    (declared once per entity — in the plugin)
       │
       ├─── hive-http               REST route generation
       │    ModelEndpointGenerator  → GET/POST/PUT/DELETE /api/v1/<model>
       │    ModelDefinitionEndpointsGenerator → GET /api/v1/model_definition
       │
       ├─── hive-essential          Service orchestration
       │    Validator pipeline   → can_create / can_read / can_update /
       │    Trigger pipeline        can_delete / can_list (IValidator)
       │    AccessMode checks    → Before / InsteadOf / After (Trigger)
       │
       ├─── hive-db-sqlite          Persistence
       │    RepositoryImplSqlite → CREATE / READ / UPDATE / DELETE SQL
       │    ModelCache           → LRU + TTL (1 week, 10 000 entries / 1 GB)
       │
       └─── frontend/api.js         Frontend rendering
            loadModelDefinition()  → cached in localStorage (3h TTL)
            renderEntityList()     → dynamic table with column selector
            renderEntityForm()     → dynamic form with FK resolution

Module Breakdown — 44k LOC, C++23

Each module has a single primary responsibility and explicit interface contracts. Dependencies flow in one direction only.

ModuleSource PathPrimary ResponsibilityKey Types
hive-app src/hive-app/ Application entry point — CLI parsing, config loading, plugin factory registration, HTTP server start Main.cpp, AppConfig
hive-http src/hive-http/ Crow-based HTTP server. Six endpoint generator classes. Routes are generated from ModelDefinition metadata at startup. ModelEndpointGenerator, AuthEndpointsGenerator, ModelDefinitionEndpointsGenerator, WebEndpointsGenerator, InfoHealthEndpointsGenerator, SuperAdminEndpointsGenerator
hive-essential src/hive-essential/ Service orchestration layer — auth context resolution, validator dispatch, trigger pipeline, cache coordination, response serialisation AccessTokenContext, CrudlService
hive-model src/hive-model/ + include/hive-model/ Core metadata types — the shared contract between all layers ModelDefinition, ColumnDefinition, ColumnDefinitionFlag, Crudl, TriggerPhase
hive-api include/hive-api/ Public plugin API — interfaces and base classes used by plugin authors Plugin, IRepository, IValidator, Trigger, Job, JobConfig, OperationResult
hive-orm src/hive-orm/ ORM entity definitions, column name constants (BaseColumns-style), serialisation boundaries BaseModel, column constant structs
hive-db-sqlite src/hive-db-sqlite/ Full SQLite persistence: repository implementation, migration executor, LRU cache, integrity verification RepositoryImplSqlite, SqliteRepositoryFactory, SqliteDatabaseMigration, ModelCache
hive-scheduler src/hive-scheduler/ Cron-expression-based job scheduler with 4 fixed worker threads and persisted run history CronScheduler, ScheduledJobEntry
hive-plugin-core src/hive-plugin-core/ Platform-critical foundation — user/session/auth, audit logs, job tables, platform models CorePlugin, UserRole, UserStatus, AccessMode
hive-plugin-slip-box src/hive-plugin-slip-box/ Zettelkasten / knowledge-graph domain — notes, links, tags, maps, sources, concepts SlipBoxPlugin, HtmlExportJob
hive-plugin-repetition src/hive-plugin-repetition/ Spaced repetition (SuperMemo-style) — sessions, reviews, predictions RepetitionPlugin
hive-plugin-dictionary src/hive-plugin-dictionary/ Lexicon management — terms, aliases, links, visit history, understanding levels DictionaryPlugin
frontend/ frontend/ Metadata-driven CRUD UI — vanilla ES modules, no build chain. Reads /api/v1/model_definition at runtime. api.js, auth.js, crud.js, schemas.js, state.js, navigation.js, dom.js, explore.js, init.js, conf.js, actions.js

Full Layer Diagram with C++ Class Names

┌──────────────────────────────────────────────────────────────────────┐
│                          USER / CLIENT                               │
│  Web browser  ·  API client  ·  cURL  ·  Admin dashboard            │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │ HTTP / JSON
                                 ▼
┌──────────────────────────────────────────────────────────────────────┐
│                     FRONTEND LAYER  (frontend/)                      │
│  11 ES module files — no build toolchain required                    │
│                                                                      │
│  api.js          — apiFetch(), QueryParams, list_all_entities()      │
│  auth.js         — login(), logout(), register(), refreshToken()     │
│  crud.js         — renderEntityList(), renderEntityForm()            │
│                    renderEntityRead(), executeCustomAction()         │
│  schemas.js      — loadModelDefinition()  (3h localStorage cache)    │
│  navigation.js   — buildNav() from model_definition groups           │
│  state.js        — global auth state, current model, page number     │
│  explore.js      — SlipBox graph exploration view                    │
│  actions.js      — custom action button dispatch                     │
│  init.js         — app bootstrap and routing                         │
│  dom.js          — shared DOM helpers                                │
│  conf.js         — host/port configuration                           │
│                                                                      │
│  Token refresh: proactive 60s before expiry via apiFetch()           │
│  FK label cache: 24h localStorage keyed by model+id                 │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │ REST calls
                                 ▼
┌──────────────────────────────────────────────────────────────────────┐
│               HTTP / TRANSPORT LAYER  (src/hive-http/)               │
│                                                                      │
│  Crow web framework — fast asynchronous C++ HTTP                     │
│                                                                      │
│  ModelEndpointGenerator          /api/v1/<model>  (all CRUD ops)     │
│  AuthEndpointsGenerator          /api/v1/auth/*                      │
│  ModelDefinitionEndpointsGenerator /api/v1/model_definition          │
│  WebEndpointsGenerator           /web/*  (static file serving)       │
│  InfoHealthEndpointsGenerator    /health  /info                      │
│  SuperAdminEndpointsGenerator    /api/v1/super_admin/*               │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │ dispatched to service layer
                                 ▼
┌──────────────────────────────────────────────────────────────────────┐
│          SERVICE ORCHESTRATION LAYER  (src/hive-essential/)          │
│                                                                      │
│  1. Parse and validate HTTP request body / query params              │
│  2. Verify Bearer token → resolve AccessTokenContext                 │
│     {user_id, role (UserRole), session_id, expiry}                   │
│  3. Lookup ModelDefinition → check operation is enabled              │
│  4. AccessMode gate: check global server access mode                 │
│  5. Run IValidator chain:                                            │
│     can_create / can_read / can_update / can_delete / can_list()     │
│     → returns OperationResult {int status, string error}             │
│     → first non-ok result aborts with HTTP status from result        │
│  6. Run Trigger pipeline — TriggerPhase::Before                      │
│     Sorted by priority (lower value = earlier execution)             │
│     Trigger::run_before_or_after(ctx, fields) → optional<fields>     │
│  7. Trigger — TriggerPhase::InsteadOf (if registered)               │
│     Trigger::run_instead_of_create / _read / _update / _delete /    │
│     _list() replaces the default repository call                     │
│  8. Repository call (if no InsteadOf):                               │
│     IRepository::create / read / update / remove / list()            │
│  9. Trigger — TriggerPhase::After                                    │
│     Post-persistence: history records, derived data, index updates   │
│ 10. Cache: invalidate or update ModelCache entries for affected ID    │
│ 11. Serialise result to JSON — return HTTP response                  │
└───────────┬────────────────────────────┬─────────────────────────────┘
            │ uses                       │ uses
            ▼                           ▼
┌──────────────────────┐    ┌────────────────────────────────────────┐
│   METADATA LAYER     │    │      PLUGIN / DOMAIN LAYER             │
│  (src/hive-model/,   │    │  (src/hive-plugin-*/                   │
│   include/hive-api/) │    │                                        │
│                      │    │  CorePlugin       — always loaded       │
│  ModelDefinition     │    │  SlipBoxPlugin    — depends on core    │
│  ColumnDefinition    │    │  RepetitionPlugin — depends on slipbox │
│  ColumnDefinitionFlag│    │  DictionaryPlugin — depends on core    │
│  Crudl enum          │    │                                        │
│  TriggerPhase enum   │    │  Each plugin contributes:             │
│  AccessMode enum     │    │  ┌─ ModelDefinition entries            │
│  UserRole enum       │    │  ├─ SQL migrations (V{n}__name.sql)    │
│  UserStatus enum     │    │  ├─ IValidator implementations         │
│  IRepository         │    │  ├─ Trigger implementations            │
│  IValidator          │    │  ├─ Job implementations                │
│  Trigger             │    │  └─ Custom named queries               │
│  Job / JobConfig     │    │                                        │
│  OperationResult     │    │  close_for_changes() called after      │
│  Plugin              │    │  registration — runtime immutability   │
└──────────┬───────────┘    └─────────────┬──────────────────────────┘
           └──────────────┬───────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────────────┐
│              PERSISTENCE LAYER  (src/hive-db-sqlite/)                │
│                                                                      │
│  RepositoryImplSqlite   — full CRUD, typed SQL generation            │
│  SqliteRepositoryFactory — factory per model name                    │
│  SqliteDatabaseMigration — applies V{n}__name.sql in order           │
│                            verifies SHA-256 + chain hash             │
│  ModelCache             — LRU eviction + 1-week TTL                  │
│                           10 000 entry / 1 GB capacity               │
│                           std::shared_mutex for concurrent reads      │
│                           stats: hits/misses/evicted_lru/expired     │
└──────────────────────────────────┬───────────────────────────────────┘
                                   │ SQL (via SQLiteCpp wrapper)
                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│                           DATABASE                                   │
│  SQLite — embedded, single-file, zero-administration                 │
│  PostgreSQL — planned (migration classes already exist for Core)     │
│                                                                      │
│  core plugin:  user, access_token, refresh_token, login_session,     │
│                auth_log, api_log, super_admin_log, history, error,   │
│                alert, job_entry, job_run                              │
│  slip_box:     note, map, link, tag, flag, pinned_note, concept,     │
│                source, wanted_note, idea, project, task              │
│  repetition:   repetition_session, repetition_review,                │
│                repetition_prediction                                  │
│  dictionary:   dictionary_map, dictionary_term, …alias, …source,    │
│                …link, …url, …visit, …understanding, …annotation      │
└──────────────────────────────────────────────────────────────────────┘

Plugin Dependency Graph

PluginRegistry performs a topological sort of plugin dependencies before applying migrations or building service registries. Cyclic or missing dependencies throw CyclicDependencyException / MissingDependencyException at startup — preventing silent misconfiguration.

                     ┌─────────────────────────────┐
                     │         CorePlugin           │
                     │                             │
                     │  auth · session · audit      │
                     │  jobs · platform models      │
                     │  always loaded               │
                     └──────┬──────────────┬────────┘
                            │              │
                   depends  │              │  depends
                            ▼              ▼
          ┌─────────────────────┐   ┌─────────────────────────┐
          │    SlipBoxPlugin    │   │   DictionaryPlugin       │
          │                     │   │                         │
          │  notes · maps       │   │  terms · aliases        │
          │  links · tags       │   │  visits · understand.   │
          │  HtmlExportJob      │   │  fulltext search        │
          └──────┬──────────────┘   └─────────────────────────┘
                 │
        depends  │
                 ▼
      ┌─────────────────────────┐
      │    RepetitionPlugin     │
      │                         │
      │  sessions · reviews     │
      │  predictions · SM-algo  │
      └─────────────────────────┘

Resolution order (topological):
  1. CorePlugin
  2. SlipBoxPlugin   (after Core)
  3. DictionaryPlugin (after Core)
  4. RepetitionPlugin (after SlipBox)

Dependency declaration in plugin code

// Each plugin declares its dependencies in its constructor
SlipBoxPlugin::SlipBoxPlugin() {
    add_dependency("core");       // PluginRegistry validates this exists
}

RepetitionPlugin::RepetitionPlugin() {
    add_dependency("slip_box");   // transitively also depends on core
}

DictionaryPlugin::DictionaryPlugin() {
    add_dependency("core");
}

Generated CRUD Request Lifecycle — Full Detail

Every request to a generated model endpoint follows this exact pipeline. The order is deterministic and enforced by hive-essential.

1

HTTP Ingress — ModelEndpointGenerator

Crow route registered for GET/POST/PUT/DELETE /api/v1/<model> fires. Path variables (:id), query string, and JSON body are parsed. Unsupported methods return 405 Method Not Allowed.

2

Access Token Verification → AccessTokenContext

The Authorization: Bearer <token> header is read and validated against the access_token table. On success, an AccessTokenContext is produced:

struct AccessTokenContext {
    int         user_id;
    UserRole    role;      // Guest=0 Reader=1 Editor=2 Reviewer=3
    std::string session_id;//  Admin=4 SuperAdmin=5 System=100
    time_t      expiry;
};

Guest requests (no token) produce a context with role = UserRole::Guest. Expired or invalid tokens return 401 Unauthorized.

3

ModelDefinition Lookup + Operation Check

The model name from the URL is used to look up its ModelDefinition in the plugin registry. The requested Crudl operation is checked against ModelDefinition::rest_operations. If not enabled, returns 405 immediately — before any auth or validator check.

// Crudl enum values:
// Undefined=0, Create=1, Read=2, Update=3, Delete=4, List=5
4

Global AccessMode Gate

The server's access_mode setting is checked. The 9-level AccessMode enum controls what requests are permitted globally, independently of per-model validators:

ValueNamePermitted
0MaintenanceModeNothing (server in maintenance)
1AdminFullAccessAdmins only, full CRUD
2AdminAndSuperUserFullAccessAdmins + SuperAdmins
3AuthenticatedReadOnlyAuthenticated: read; others: none
4AuthenticatedFullAccessAuthenticated users full CRUD
5PublicReadOnlyAnyone can read; authenticated can write
6PublicCreateAndReadPublic create + read; authenticated full
7PublicCreateReadUpdatePublic create/read/update
8PublicFullAccessAnyone can do anything (default config)
5

IValidator Pipeline

All IValidator implementations registered for this model execute in registration order. Each validator receives the full AccessTokenContext and request payload. The first non-OK result short-circuits and returns the error with the HTTP status from OperationResult.status.

// IValidator interface
struct OperationResult {
    int         status;  // HTTP status code
    std::string error;   // human-readable message
    bool ok() const { return status == 0 || status == 200; }
};

// Built-in result macros
ok_result                 // {0, ""}
status_403_forbidden      // {403, "Forbidden"}
status_405_unsupported    // {405, "Unsupported operation"}

class IValidator {
public:
    virtual OperationResult can_create(AccessTokenContext&, Fields&) = 0;
    virtual OperationResult can_read  (AccessTokenContext&, int id)  = 0;
    virtual OperationResult can_update(AccessTokenContext&, int id, Fields&) = 0;
    virtual OperationResult can_delete(AccessTokenContext&, int id)  = 0;
    virtual OperationResult can_list  (AccessTokenContext&, QueryParams&) = 0;
};
6

Trigger — TriggerPhase::Before

Triggers registered for TriggerPhase::Before fire in priority order (lower integer = higher priority). Each receives the request fields and may modify them before persistence. Any trigger may return std::nullopt to abort the operation.

// Trigger constructor signature
Trigger(
    std::string name,
    int priority,                       // lower = earlier
    std::set<Crudl> operations,         // which ops trigger fires for
    TriggerPhase phase,                 // Before / After / InsteadOf / Around
    std::string table,                  // model name, or "*" for all models
);

// TriggerPhase enum
// Before=0, After=1, InsteadOf=2, Around=3

// Before/After phase handler:
std::optional<Fields> run_before_or_after(
    AccessTokenContext& ctx,
    Fields& fields
);
7

Trigger — TriggerPhase::InsteadOf (optional)

InsteadOf triggers completely replace the default repository call. They have dedicated virtual methods per operation. Used by the Slipbox search system, Repetition due-item selection, and Dictionary fulltext search.

// InsteadOf phase handlers (override only the operations you need)
virtual std::optional<Entity>  run_instead_of_create(...) { return nullopt; }
virtual std::optional<Entity>  run_instead_of_read(...)   { return nullopt; }
virtual std::optional<Entity>  run_instead_of_update(...) { return nullopt; }
virtual std::optional<bool>    run_instead_of_delete(...) { return nullopt; }
virtual std::optional<Entities> run_instead_of_list(...)  { return nullopt; }
8

Repository — RepositoryImplSqlite

If no InsteadOf trigger replaced the call, RepositoryImplSqlite executes the database operation. All operations are transactional via SQLiteCpp. The IRepository interface used by the service layer:

class IRepository {
public:
    virtual Entity  create(Fields& fields, std::string& error) = 0;
    virtual Entity  read  (int id,         std::string& error) = 0;
    virtual Entity  update(int id, Fields& fields, std::string& error) = 0;
    virtual bool    remove(int id,         std::string& error) = 0;
    virtual Entities list (QueryParams& qp, std::string& error) = 0;
    virtual Entities list_in_ids(std::vector<int>& ids, ...) = 0;
    virtual std::vector<int> list_ids(QueryParams& qp, ...) = 0;
    virtual Fields request_to_entity_fields(Request& req) = 0;
};
9

Cache Invalidation — ModelCache

After a write operation, the ModelCache for the affected model is updated: the specific entry is invalidated on update/delete; new entries are optionally pre-populated on create if set_cached_after_create(true) is set in the ModelDefinition. List caches are invalidated after any write.

// ModelCache characteristics
// Eviction: LRU
// Default TTL: 1 week (604 800 seconds)
// Max entries: 10 000
// Max memory: 1 GB
// Thread safety: std::shared_mutex (many readers / one writer)
// Stats tracked: hits, misses, evicted_lru, expired, shrinks
10

Trigger — TriggerPhase::After

After-triggers run post-persistence. Common uses: writing history records to the history table (Core plugin trigger registered for table "*" = all models), updating fulltext search indexes, triggering cross-model derived value updates, and scheduling follow-up automation.

11

Response Serialisation

The result entity or entity list is serialised to JSON using nlohmann/json. Column metadata controls which fields are included based on ColumnDefinitionFlag::HIDDEN and INTERNAL flags. The response is returned with appropriate HTTP status code.


Application Bootstrap — Full Sequence

hive_app start [--port 9000] [--host http://localhost] [-s ./frontend]
    │
    ▼  1. Parse command line arguments + load configuration/hive.properties
       Keys: host, port, frontend_port, db_path, frontend_path,
             access_mode, registration_mode, default_user_role,
             max_log_level, allowed_plugins,
             access_token_expires_in, refresh_token_expires_in,
             refresh_token_rotation_threshold_in
    │
    ▼  2. Register plugin factories in Main.cpp
       CorePlugin, SlipBoxPlugin, RepetitionPlugin, DictionaryPlugin
       (each factory is a lambda returning unique_ptr<Plugin>)
    │
    ▼  3. PluginRegistry::get_plugin_names_sorted_by_dependencies()
       Topological sort — throws CyclicDependencyException or
       MissingDependencyException on bad configuration
       Result: [core, slip_box, dictionary, repetition]
    │
    ▼  4. Migration bootstrap — for each plugin in dependency order:
       SqliteDatabaseMigration applies all pending V{n}__name.sql files
       Naming regex: V(\d+)__([a-zA-Z0-9_]+).sql
       Each migration: compute SHA-256 checksum of SQL content
       Chain hash: SHA-256( prev_chain_hash + current_checksum )
       On hash mismatch → startup fails with integrity error
    │
    ▼  5. Service registry construction — for each plugin:
       plugin.register_model()    → ModelDefinitionRegistry
       plugin.register_validator()→ ValidatorRegistry
       plugin.register_trigger()  → TriggerRegistry
       plugin.register_query()    → QueryRegistry
       plugin.register_job()      → JobRegistry
       plugin.close_for_changes() → Plugin becomes immutable (runtime lock)
    │
    ▼  6. HTTP route generation — one pass over ModelDefinitionRegistry:
       ModelEndpointGenerator         → CRUD routes per model
       AuthEndpointsGenerator         → /api/v1/auth/*
       ModelDefinitionEndpointsGenerator → /api/v1/model_definition
       WebEndpointsGenerator          → /web/* (static file serving)
       InfoHealthEndpointsGenerator   → /health  /info
       SuperAdminEndpointsGenerator   → /api/v1/super_admin/*
    │
    ▼  7. Admin bootstrap (first run only)
       If no users exist: generate random admin password
       Write to pw.txt in working directory
       Create initial admin user with role SuperAdmin
    │
    ▼  8. CronScheduler startup
       JobRegistry.sync_from_db() — load job_entry table
       For each job: parse cron expression, schedule ScheduledJobEntry
       Start fixed thread pool: 4 worker threads
       ScheduledJobEntry: {job_name, next_run, last_run, is_running}
    │
    ▼  Running — Crow HTTP server accepting connections on port 9000
       CronScheduler ticking in background (4 threads)

CronScheduler Internals

The CronScheduler is the background automation engine. It manages a fixed pool of 4 worker threads, persists job run history to the job_run table, and supports missed-run recovery.

Architecture

CronScheduler
├── thread_pool: 4 fixed threads
├── ScheduledJobEntry per job:
│   ├── job_name: string
│   ├── next_run: time_t
│   ├── last_run: time_t
│   └── is_running: bool
├── Priority queue by next_run
└── job_run table: persisted history
    (output string, start/end time,
     success flag)

Job constructor

Job(
    string name,
    string description,
    string cron_expression,  // standard 5-field cron
    bool   enabled_by_default = true,
    bool   run_once_when_missed = true
);

// Job implementation
virtual string run(JobConfig& config) = 0;
// return value: log message written to job_run.output

Built-in jobs (Core plugin)

Job classDefault cronPurpose
DatabaseVacuumJob0 2 * * 0SQLite VACUUM — reclaim space weekly
AuthLogCleanupJob0 3 * * *Purge old auth_log entries
ApiLogCleanupJob0 3 * * *Purge old api_log entries
HistoryCleanupJob0 4 * * *Prune old CRUD history records
ErrorCleanupJob0 4 * * *Purge old error log entries
SuperAdminLogCleanupJob0 5 * * 0Purge super_admin_log weekly

Domain plugin jobs

Job classPluginPurpose
HtmlExportJobslip_boxFull static HTML export of the Slipbox knowledge graph with navigation

run_once_when_missed = true means: if the server was down when a job was due, it fires once on next startup to catch up. The job_entry table stores enabled/disabled state per job, which the admin can toggle via the SuperAdmin UI.


Migration Integrity System

Hive guarantees that applied migrations are never silently modified after the fact. The integrity mechanism uses two complementary hash checks.

Migration file naming convention:
  V{sequence_number}__{name}.sql
  Example: V1__initial_schema.sql
           V2__add_user_role_column.sql
  Regex:   V(\d+)__([a-zA-Z0-9_]+).sql

For each migration, on first application:
  1. Compute SHA-256 of the SQL file content
  2. chain_hash = SHA-256( prev_chain_hash || current_file_hash )
     (|| = concatenation; first migration uses empty string as prev)
  3. Store: (filename, file_hash, chain_hash, applied_at) in migration table

On subsequent startups (already-applied migrations):
  4. Re-read the SQL file, recompute file_hash
  5. If file_hash differs from stored: startup FAILS — migration was tampered
  6. Recompute chain_hash with stored sequence — if differs: FAILS
     (catches reordering attacks where individual files look untouched)

Benefits:
  - Prevents accidental retroactive edits to migrations (a common ops mistake)
  - Detects reordering (chain hash covers the sequence, not just each file)
  - Each plugin has its own migration sequence, but global order is enforced
    by plugin dependency sort

Note: PostgreSQL migration classes also exist (SqliteDatabaseMigration and
      PostgresDatabaseMigration share a common interface), enabling the
      planned multi-database roadmap.

Schema-Driven Frontend Flow

The frontend is entirely driven by server-returned metadata. No model-specific code exists in the JS files — adding a new model to a plugin automatically produces a working CRUD screen in the frontend.

Browser opens /web/index.html
    │
    ▼ init.js: check localStorage for existing auth state (state.js)
    │
    ▼ schemas.js: loadModelDefinition()
      GET /api/v1/model_definition
      Cache result in localStorage for 3 hours (key: "model_definition")
      Returns: array of ModelDefinition objects per registered model
    │
    ▼ navigation.js: buildNav()
      Group models by ModelDefinition.group field
      Render top-level navigation entries per group
    │
    ▼ User navigates to a model (e.g. "Notes" → "note" model)
      state.js: set current_model = "note", current_page = 1
    │
    ▼ crud.js: renderEntityList()
      GET /api/v1/note?page=1&page_size=20
      Build column list from ModelDefinition.columns (visible only)
      Render sortable, filterable table
      renderColumnSelector() — user can toggle visible columns
      FK columns: resolveForeignKeyValue() → cached label lookup
        Cache: localStorage key "fk_cache_{model}_{id}", TTL 24h
    │
    ▼ User clicks entity → renderEntityRead()
      GET /api/v1/note/:id
      Render read-only view with FK labels
      If custom_read_actions defined: render CustomAction buttons
      executeCustomAction() → POST /api/v1/note/:id/action/{action_name}
    │
    ▼ User clicks Edit → renderEntityForm()
      Render form inputs based on column types from ColumnDefinitionFlag:
        TEXT flag      → <input type="text">
        TEXTAREA flag  → <textarea>
        INTEGER flag   → <input type="number">
        REAL flag      → <input type="number" step="any">
        BOOL flag      → <input type="checkbox">
        DATETIME flag  → <input type="datetime-local">
        FOREIGN_KEY    → <select> populated from referenced model list
      READONLY columns: shown but not editable
      HIDDEN/INTERNAL: not shown at all
    │
    ▼ Auth: apiFetch() in api.js
      All API calls go through apiFetch()
      If access_token expires in < 60 seconds: proactively call
        POST /api/v1/auth/refresh_token with refresh_token
      On 401: attempt refresh, then retry original request once
      On failure: redirect to login screen (auth.js: login())
    │
    ▼ list_all_entities() — used for FK dropdowns
      Auto-pages through all records up to 1 000 total
      Uses QueryParams class: page, page_size, sort, order, filter

Explicit Architectural Trade-offs

No architecture is without trade-offs. Hive's decisions are deliberate and documented here for transparency.

Metadata-driven vs explicit routes

Benefit: Adding a model to a plugin automatically produces REST endpoints, frontend screens, validation hooks, and cache behaviour — zero CRUD boilerplate.
Cost: ModelDefinition quality and flag conventions become critical engineering discipline. A misconfigured flag affects every layer simultaneously.

Trigger pipeline vs direct business logic

Benefit: Clean separation of cross-cutting concerns (history, indexes, derived values) from core persistence. Triggers compose cleanly without modifying core code.
Cost: Complex trigger chains (Priority × Phase × Model) can be harder to debug than direct procedural code. The Around trigger phase (TriggerPhase::Around=3) is reserved for future use.

Plugin modularity vs startup complexity

Benefit: Each plugin is a coherent vertical slice — its own models, migrations, validators, triggers, jobs. Independent development and deployment possible.
Cost: Topological dependency resolution, migration ordering, and service registry construction must be managed carefully at startup.

SQLite-first embedded DB

Benefit: Zero infrastructure overhead, single-file deployment, embedded operation ideal for personal servers and small teams. No separate database process required.
Cost: Write contention at high concurrency. PostgreSQL migration classes already exist (SqliteDatabaseMigration / PostgresDatabaseMigration share an interface) — multi-DB support is on the active roadmap.

Vanilla JS frontend vs framework

Benefit: Zero build toolchain, zero npm dependencies, directly readable source, deployed by simply copying files. No version churn from framework upgrades.
Cost: Complex UI interactions require more manual DOM management. ES modules are modern (2017+) but require a reasonably current browser.

Generic CRUD pipeline vs per-entity handlers

Benefit: Every entity behaves consistently — the same pipeline enforces auth, triggers, and caching for all of them. Debugging is predictable.
Cost: Advanced cases must be modelled through InsteadOf triggers or custom queries rather than quick one-off route handlers. This forces good design but adds indirection.

Ready to extend Hive?

Read the Developer Guide for a practical walkthrough of creating your first plugin with models, migrations, validators, and triggers.