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 — ~45k Lines of C++23 (~68k LOC total)

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
cronq (in hive-api) src/hive-api/hive/api/cronq/ Cron-expression-based job scheduler with 4 fixed worker threads and persisted run history CronScheduler, ScheduledJobEntry, JobConfig
hive-util src/hive-util/ Shared utilities — SHA-256 hashing (OpenSSL), string/time helpers Utils
Core plugin (in hive-api) src/hive-api/hive/plugins/core/ Platform-critical foundation — user/session/auth, audit logs, job tables, platform models. Ships both SQLite and PostgreSQL migration scripts. 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/superadmin/*                │
└────────────────────────────────┬─────────────────────────────────────┘
                                 │ 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)        │
│  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 scripts 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 — groundwork (Core migration scripts + DatabaseType)     │
│                                                                      │
│  core (13):       user, team, team_member, access_token,             │
│                   refresh_token, login_session, auth_log, api_log,   │
│                   super_admin_log, history, error,                   │
│                   job_entry, job_run                                 │
│  slip_box (26):   note, map, content, link, tag, tag_type, property, │
│                   flag, pinned_note, source, wanted_note, idea,      │
│                   project, task, alert, annotation, collection,      │
│                   collection_item, map_collection, question, term,   │
│                   test, test_attempt, test_attempt_answer, url, …    │
│  repetition (13): r_session, r_review, r_global_setting,             │
│                   r_user_setting, r_session_metric, r0/r2/r4_state,  │
│                   r18_state, r18_perf_agg, r18_prediction_log,       │
│                   r18_adaptive_parameters, …                         │
│  dictionary (20): dictionary_map, dictionary_term, …term_alias,      │
│                   …term_visit, …term_understanding, …link, …source,  │
│                   …url, …search, …review, …index, …note, …tag,       │
│                   …flag, …pinned_term, …state_18, + type tables      │
└──────────────────────────────────────────────────────────────────────┘

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               │
                     └──────┬──────────────┬────────┘
                            │              │
                 (implicit) │              │ (implicit)
                   depends  │              │  depends
                            ▼              ▼
          ┌─────────────────────┐   ┌─────────────────────────┐
          │    SlipBoxPlugin    │   │   DictionaryPlugin       │
          │                     │   │                         │
          │  notes · maps       │   │  terms · aliases        │
          │  links · tags       │   │  visits · understand.   │
          │  HtmlExportJob      │   │  fulltext search        │
          └──────┬──────────────┘   └─────────────────────────┘
                 │
       explicit  │
        depends  │
                 ▼
      ┌─────────────────────────┐
      │    RepetitionPlugin     │
      │                         │
      │  sessions · reviews     │
      │  r18 predictions        │
      └─────────────────────────┘

Resolution order (topological):
  1. CorePlugin
  2. SlipBoxPlugin    (after Core)
  3. DictionaryPlugin (after Core)
  4. RepetitionPlugin (after SlipBox — declared: {"slip_box"})

Dependency declaration in plugin code

// Dependencies are passed to the Plugin base-class constructor.
// Every non-core plugin gets an implicit dependency on "core":
Plugin::Plugin(const std::string& name_, const std::string& description_,
               std::vector<string> apps_,
               const std::vector<std::string>& plugins_which_this_plugin_depends_on_)
    : name(name_), description(description_),
      apps(std::move(apps_)),
      plugins_which_this_plugin_depends_on(plugins_which_this_plugin_depends_on_)
{
    if (get_name() != "core")
    {
        plugins_which_this_plugin_depends_on.emplace_back("core");
    }
}

// Additional dependencies can be listed explicitly in the constructor call.
// PluginRegistry::get_plugin_names_sorted_by_dependencies() validates the
// graph and throws CyclicDependencyException / MissingDependencyException.

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 (tokens are stored SHA-256-hashed). On success, an AccessTokenContext is produced:

struct AccessTokenContext {
    identification user_id{};
    std::string    msg;            // diagnostic message
    int            status{};       // HTTP status: 200 = ok
    bool           system{false};  // true = internal system token

    bool ok() const { return status == 200; }
    bool ko() const { return !ok(); }
    bool is_system() const;
};

The user's UserRole (Guest=0 … SuperAdmin=5, System=100) is resolved from the user table via user_id. Guest requests (no token) resolve to UserRole::Guest. Expired, revoked, 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
0MaintenanceModeAPI disabled — no access for any user
1AdminsReadOnlyOnly Admins can read; everyone else denied
2AdminsReadWriteOnly Admins can read/write; everyone else denied
3AuthenticatedReadOnlyToken required; authenticated users read-only
4AuthenticatedReadWriteToken required; read/write according to roles
5AuthenticatedFullAccessToken required; full access, roles ignored
6PublicReadOnlyAuthenticatedReadOnlyGuests read; authenticated also read-only
7PublicReadOnlyAuthenticatedReadWriteGuests read; authenticated read/write by role
8PublicFullAccessFully open API — guests and users unrestricted (code default)
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;  // 0 = OK, other number = error (HTTP status)
    std::string error;   // error description, empty if OK
    bool ok() const { return status == 0; }
    bool ko() const { return !ok(); }
};

// Built-in result macros
ok_result                 // {0, ""}
status_403_forbidden      // {403, "You are not authorized..."}
status_405_unsupported    // {405, "Unsupported operation"}

class IValidator {
public:
    virtual OperationResult can_create(DbPtr& db, AccessTokenContext& token,
                                       entity_fields& ef) const = 0;
    virtual OperationResult can_read  (DbPtr& db, AccessTokenContext& token,
                                       identification id) const = 0;
    virtual OperationResult can_update(DbPtr& db, AccessTokenContext& token,
                                       entity_fields& ef,
                                       entity_fields& old_fields) const = 0;
    virtual OperationResult can_delete(DbPtr& db, AccessTokenContext& token,
                                       identification id) const = 0;
    virtual OperationResult can_list  (DbPtr& db, AccessTokenContext& token,
                                       string_map& filter) const = 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 scripts
       Naming regex: V(\d+)__([a-zA-Z0-9_]+) — embedded C++ SQL literals
       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/superadmin/*
    │
    ▼  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
CleanupJob@dailyDeletes old api_log, history (Read/List ops), login_session, and access_token records past a per-type age threshold
CleanupHistoryOrphansJob@dailyRemoves history entries whose referenced record no longer exists
VacuumJob@monthlySQLite VACUUM — reclaims space; disabled by default, run_once_when_missed=false
TestJobconfigurableDebug/test job for scheduler verification

Domain plugin jobs

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

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 naming convention:
  V{sequence_number}__{name}
  Example: V1__create_user
           V2__create_team
  Regex:   V(\d+)__([a-zA-Z0-9_]+)
  Migrations ship as C++ raw string literals in per-plugin
  *MigrationScripts.cpp files (e.g. CoreSQLiteMigrationScripts.cpp).

For each migration, on first application:
  1. Compute SHA-256 of the migration's SQL content
  2. chain_hash = SHA-256( prev_chain_hash || current_checksum )
     (|| = concatenation; first migration uses empty string as prev)
  3. Store: (name, checksum, chain_hash, applied_at) in schema history

On subsequent startups (already-applied migrations):
  4. Recompute the checksum of the embedded SQL content
  5. If it differs from the stored value: startup FAILS — migration changed
  6. Recompute chain_hash over the stored sequence — if differs: FAILS
     (catches reordering, where individual migrations look untouched)

Execution is transactional — a failed migration rolls back cleanly.

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

Shipped migrations: core V1–V14 · slip_box V1–V33 · dictionary V1–V29 ·
                    repetition V1–V24  (100 total)

Note: the Core plugin ships its migrations in two SQL dialects —
      CoreSQLiteMigrationScripts and CorePostgreSQLMigrationScripts —
      groundwork for the planned multi-database support (DatabaseType).

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 phase (TriggerPhase::Around=3, "before and after together") exists but is used sparingly.

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 groundwork already exists (DatabaseType::PostgreSQL, CorePostgreSQLMigrationScripts, db_* config keys) — 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.