First-Class Plugin Architecture

Plugins are the primary extension mechanism in Hive. Each plugin contributes models, migrations, validators, triggers, jobs, and queries as a single coherent vertical slice — not scattered patches across core files. The four built-in plugins cover authentication, knowledge management, spaced repetition, and lexicon management.

What a plugin can register

The Plugin base class (in include/hive-api/hive/api/Plugin.hpp) provides all registration methods. After startup, close_for_changes() is called — the plugin becomes immutable at runtime.

Registration methodWhat it registersUsed by
register_model(ModelDefinition) An entity definition: columns, CRUD ops, UI hints, cache config, group HTTP route generation, frontend rendering, persistence, validation
register_migrations(vector<Migration>) Ordered SQL migration files (V{n}__name.sql) for this plugin's schema Applied at startup in dependency order with SHA-256 chain-hash integrity
register_validator(model_name, IValidator) Pre-operation security and invariant gate for a specific model Validator pipeline (step 5 of request lifecycle)
register_trigger(Trigger) Before / InsteadOf / After hook for one or more CRUD operations Trigger pipeline (steps 6–9 of request lifecycle)
register_job(Job) A named cron-scheduled background task CronScheduler (4 worker threads)
register_query(name, Query) Named reusable query — complex lookups shared across validators/triggers/jobs Called via QueryRegistry::call(name, args)

Plugin Lifecycle & Dependency Resolution

Main.cpp: register plugin factories
   new CorePlugin(), new SlipBoxPlugin(), new RepetitionPlugin(), new DictionaryPlugin()
          │
          ▼
PluginRegistry::get_plugin_names_sorted_by_dependencies()
   Topological sort of declared dependency graph
   Throws CyclicDependencyException   — if A→B→A detected
   Throws MissingDependencyException  — if declared dep not registered
   Result: [core, slip_box, dictionary, repetition]
          │
          ▼
For each plugin in sorted order:
  SqliteDatabaseMigration::apply_all()
    · Scan V{n}__name.sql files
    · Compute SHA-256 per file + rolling chain_hash
    · Apply only new migrations (compare to migration table)
    · On hash mismatch: throw — startup aborts
          │
          ▼
For each plugin in sorted order:
  plugin.register_models()     → ModelDefinitionRegistry
  plugin.register_validators() → ValidatorRegistry
  plugin.register_triggers()   → TriggerRegistry (sorted by priority)
  plugin.register_jobs()       → JobRegistry
  plugin.register_queries()    → QueryRegistry
  plugin.close_for_changes()   → Plugin becomes read-only at runtime
          │
          ▼
HTTP route generation (one pass over ModelDefinitionRegistry)
Cron scheduler start (4 threads, ScheduledJobEntry per job)

Plugin dependency graph

core ◄── slip_box ◄── repetition
core ◄── dictionary

Core Plugin — Platform Foundation

The Core plugin provides platform-critical infrastructure required by every domain plugin. It is always loaded first and cannot be disabled. Source: src/hive-plugin-core/.

Tables registered (18 models)

TablePurpose
userUser accounts — username, password hash, role, status, registration date
access_tokenNamed persistent API tokens with expiry and revocation
refresh_tokenSession refresh tokens — hashed, rotating
login_sessionTracked login sessions — IP, user agent, expiry
auth_logAuthentication events — login success/failure, logout, token refresh
api_logAll API requests — method, path, status, duration, user
super_admin_logPrivileged operations log — only accessible to SuperAdmin
historyCRUD operation history for all models (written by global After trigger)
errorServer-side error tracking — type, message, stack, timestamp
alertSystem alerts — severity, message, acknowledged flag
job_entryPersisted job definitions — name, cron, enabled, last_run
job_runJob execution telemetry — start, end, success, output string

Key enums (Core domain)

enum class UserRole : int {
    Guest      = 0,
    Reader     = 1,
    Editor     = 2,
    Reviewer   = 3,
    Admin      = 4,
    SuperAdmin = 5,
    System     = 100
};

enum class UserStatus : int {
    Pending     = 0,
    Active      = 1,
    Deactivated = 2,
    Banned      = 3,
    Suspended   = 4,
    Deleted     = 5
};

enum class RegistrationMode : int {
    Free                  = 0,
    RequiresAdminApproval = 1,
    AdminAddsUsers        = 2
};

Core validator count

The Core plugin registers approximately 13 validators covering authentication logic, user role enforcement, token lifecycle, registration flow, and admin-only operations. These validators collectively enforce the access control matrix for all auth operations.

Core plugin SQL schema (key tables)

-- User account table
CREATE TABLE IF NOT EXISTS user (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    username        TEXT    NOT NULL UNIQUE,
    password_hash   TEXT    NOT NULL,
    role            INTEGER NOT NULL DEFAULT 1,  -- UserRole::Reader
    status          INTEGER NOT NULL DEFAULT 0,  -- UserStatus::Pending
    email           TEXT,
    created_at      TEXT    NOT NULL,
    updated_at      TEXT
);

-- Named persistent API token
CREATE TABLE IF NOT EXISTS access_token (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id         INTEGER NOT NULL REFERENCES user(id),
    token_hash      TEXT    NOT NULL UNIQUE,
    name            TEXT    NOT NULL,
    expires_at      TEXT,
    created_at      TEXT    NOT NULL,
    last_used_at    TEXT,
    revoked         INTEGER NOT NULL DEFAULT 0
);

-- Session refresh tokens (rotating)
CREATE TABLE IF NOT EXISTS refresh_token (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id         INTEGER NOT NULL REFERENCES user(id),
    token_hash      TEXT    NOT NULL UNIQUE,
    session_id      TEXT    NOT NULL,
    expires_at      TEXT    NOT NULL,
    created_at      TEXT    NOT NULL,
    used            INTEGER NOT NULL DEFAULT 0
);

-- Authentication event log
CREATE TABLE IF NOT EXISTS auth_log (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id         INTEGER REFERENCES user(id),
    event_type      TEXT    NOT NULL,  -- login_ok, login_fail, logout, refresh
    ip_address      TEXT,
    user_agent      TEXT,
    created_at      TEXT    NOT NULL
);

-- CRUD history for all models (global After trigger)
CREATE TABLE IF NOT EXISTS history (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    model_name      TEXT    NOT NULL,
    entity_id       INTEGER NOT NULL,
    operation       TEXT    NOT NULL,  -- create/update/delete
    user_id         INTEGER REFERENCES user(id),
    before_data     TEXT,              -- JSON snapshot before
    after_data      TEXT,              -- JSON snapshot after
    created_at      TEXT    NOT NULL
);

-- Job execution registry
CREATE TABLE IF NOT EXISTS job_entry (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    name            TEXT    NOT NULL UNIQUE,
    description     TEXT,
    cron_expression TEXT    NOT NULL,
    enabled         INTEGER NOT NULL DEFAULT 1,
    run_once_when_missed INTEGER NOT NULL DEFAULT 1,
    last_run_at     TEXT,
    next_run_at     TEXT
);

-- Job run telemetry
CREATE TABLE IF NOT EXISTS job_run (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    job_entry_id    INTEGER NOT NULL REFERENCES job_entry(id),
    started_at      TEXT    NOT NULL,
    finished_at     TEXT,
    success         INTEGER NOT NULL DEFAULT 0,
    output          TEXT
);

Core plugin triggers

Trigger namePhaseTableOperationsPurpose
history_trigger After "*" (all models) Create, Update, Delete Writes a JSON snapshot diff to the history table for every mutating operation across all registered models

Core plugin scheduled jobs

Job classDefault schedulePurpose
DatabaseVacuumJob0 2 * * 0 (Sun 02:00)SQLite VACUUM — compact the database file and reclaim fragmented space
AuthLogCleanupJob0 3 * * * (03:00 daily)Purge auth_log entries older than retention period
ApiLogCleanupJob0 3 * * * (03:00 daily)Purge api_log entries older than retention period
HistoryCleanupJob0 4 * * * (04:00 daily)Prune old records from the history table
ErrorCleanupJob0 4 * * * (04:00 daily)Purge old entries from the error table
SuperAdminLogCleanupJob0 5 * * 0 (Sun 05:00)Purge old entries from super_admin_log

Slipbox Plugin — Knowledge Graph

A rich knowledge management system inspired by the Zettelkasten / Slip Box methodology. Designed for structured note-taking, knowledge graphs, and linked writing. Depends on: core. Source: src/hive-plugin-slip-box/.

Tables registered (13 models)

TablePurpose
notePrimary knowledge unit — title, content (Markdown), parent, map
mapLogical grouping container for notes
linkDirectional connections between notes (source_id → target_id, type)
tagTag definitions
note_tagNote ↔ tag many-to-many junction
flagNote flagging (importance, review needed, etc.)
pinned_notePer-user pinned notes list
conceptTitle / disambiguation entity with note linkage
sourceBook / web references (title, URL, author, year)
wanted_notePlaceholder for planned-but-not-yet-written notes
ideaQuick idea capture (fleeting notes)
projectProject entity for structured work tracking
taskTask entity linked to a project

Slipbox SQL schema (note table)

CREATE TABLE IF NOT EXISTS note (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    title       TEXT    NOT NULL,
    content     TEXT,           -- Markdown
    parent_id   INTEGER REFERENCES note(id),
    map_id      INTEGER REFERENCES map(id),
    path        TEXT,           -- computed by trigger
    note_order  INTEGER NOT NULL DEFAULT 0,
    created_at  TEXT    NOT NULL,
    updated_at  TEXT,
    user_id     INTEGER REFERENCES user(id)
);

CREATE TABLE IF NOT EXISTS link (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    source_id   INTEGER NOT NULL REFERENCES note(id),
    target_id   INTEGER NOT NULL REFERENCES note(id),
    link_type   TEXT,
    description TEXT,
    created_at  TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS map (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL,
    description TEXT,
    created_at  TEXT    NOT NULL
);

Bundled JS libraries

The Slipbox plugin registers these static JS/CSS files for the frontend note editor:

  • markdown-it.min.js — Markdown renderer
  • markdown-it-emoji.min.js — emoji extension
  • highlight.min.js — syntax highlighting
  • github.min.css — code highlight theme

Slipbox triggers (~10)

Trigger namePhaseTableOpsPurpose
note_path_before_createBeforenoteCreateCompute and set the path field (ancestor chain) before persisting a new note
note_path_before_updateBeforenoteUpdateRecompute path if parent_id or map_id changed
note_children_path_after_updateAfternoteUpdateRecursively update path on all child notes when parent path changes
note_history_after_createAfternoteCreateWrite initial snapshot to note history table
note_history_after_updateAfternoteUpdateWrite before/after diff to note history
concept_alias_resolveInsteadOfconceptListResolve concept aliases during search — merge alias matches with direct concept matches
wanted_to_note_triggerAfternoteCreateIf new note title matches a wanted_note entry, automatically link/resolve it
link_cascade_deleteBeforenoteDeleteRemove all outbound and inbound links when a note is deleted
tag_cleanupAfternoteDeleteOrphan note_tag junction rows after note deletion
search_instead_of_listInsteadOfnoteListOverride list with fulltext LIKE search when q query param is present

Slipbox custom queries

Query namePurpose
get_childrenReturn direct child notes of a given parent_id
get_path_notesReturn the full ancestor chain (breadcrumb) for a note
get_linked_notesReturn all notes linked from/to a given note ID
get_notes_by_tagReturn notes with a specific tag ID

Slipbox jobs

Job classDefault schedulePurpose
HtmlExportJobConfigurableExports the entire Slipbox knowledge graph as a static HTML website with navigation, link graph, and note index. Output can be deployed as a static site.
Graph exploration
Graph exploration — Slipbox Plugin
List nodes
Note list view with column selector

Repetition Plugin — Spaced Repetition

SuperMemo-style spaced repetition system for systematic knowledge review and memorisation scheduling. Depends on: slip_box (and transitively core). Source: src/hive-plugin-repetition/.

Tables registered (3 models)

TablePurpose
repetition_sessionA review session — user, start time, status (active/completed), item count
repetition_reviewIndividual card review — session, note_id, rating (0–5), timestamp
repetition_predictionNext-review scheduling per note — interval, ease factor, due date

SQL schema

CREATE TABLE IF NOT EXISTS repetition_session (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id     INTEGER NOT NULL REFERENCES user(id),
    started_at  TEXT    NOT NULL,
    completed_at TEXT,
    status      TEXT    NOT NULL DEFAULT 'active',
    item_count  INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS repetition_review (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id  INTEGER NOT NULL
                  REFERENCES repetition_session(id),
    note_id     INTEGER NOT NULL REFERENCES note(id),
    rating      INTEGER NOT NULL,  -- 0 (fail) to 5 (perfect)
    reviewed_at TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS repetition_prediction (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    note_id         INTEGER NOT NULL UNIQUE REFERENCES note(id),
    user_id         INTEGER NOT NULL REFERENCES user(id),
    interval_days   INTEGER NOT NULL DEFAULT 1,
    ease_factor     REAL    NOT NULL DEFAULT 2.5,
    due_date        TEXT    NOT NULL,
    review_count    INTEGER NOT NULL DEFAULT 0,
    last_reviewed_at TEXT
);

Repetition triggers (4)

TriggerPhaseTableOpPurpose
review_after_createAfterrepetition_reviewCreateAfter each card rating, recompute repetition_prediction: interval, ease factor, next due date using SM-2 algorithm
session_complete_after_updateAfterrepetition_sessionUpdateWhen a session status changes to completed, set completed_at timestamp
note_delete_cascadeBeforerepetition_predictionDeleteClean up reviews and predictions when source note is deleted from Slipbox
due_items_instead_of_listInsteadOfrepetition_predictionListOverride standard list to filter by due date, with support for due/not-due/never-reviewed variants via query param

Custom queries

Query namePurpose
get_due_notesReturn notes due for review (due_date ≤ today), with note title from Slipbox join
get_session_statsReturn counts: due, not-due, never-reviewed per user

Frontend app

  • Review session interface (card-by-card)
  • Rating buttons: 0 (Again) · 1 · 2 · 3 · 4 · 5 (Perfect)
  • Session progress bar
  • Statistics panel: due / not-due / never reviewed counts
  • Served from /web/app_repetition.html

Dictionary Plugin — Lexicon Management

A full-featured lexicon and dictionary system with rich search, visit tracking, understanding levels, multi-language support, and Repetition integration. Depends on: core. Source: src/hive-plugin-dictionary/.

Tables registered (9 models)

TablePurpose
dictionary_mapThematic map — grouping container with emoji, name, description
dictionary_termPrimary term / word — title, definition (Markdown), map_id, language
dictionary_term_aliasAlternate names / spellings for a term
dictionary_sourceReference sources — book, web URL, author, year
dictionary_linkDirectional inter-term links — source term → target term, relation type
dictionary_urlExternal URL references attached to a term
dictionary_term_visitVisit history per term — user, timestamp, source (search, FK, direct)
dictionary_term_understandingPer-user understanding level per term (0=Unknown to 4=Internalised)
dictionary_annotationPersonal annotations on terms — user-specific Markdown notes

SQL schema (key tables)

CREATE TABLE IF NOT EXISTS dictionary_term (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    title       TEXT    NOT NULL,
    definition  TEXT,           -- Markdown
    map_id      INTEGER REFERENCES dictionary_map(id),
    language    TEXT    NOT NULL DEFAULT 'en',
    created_at  TEXT    NOT NULL,
    updated_at  TEXT
);

CREATE TABLE IF NOT EXISTS dictionary_term_alias (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    term_id     INTEGER NOT NULL
                  REFERENCES dictionary_term(id),
    alias       TEXT    NOT NULL,
    created_at  TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS dictionary_term_understanding (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    term_id     INTEGER NOT NULL
                  REFERENCES dictionary_term(id),
    user_id     INTEGER NOT NULL REFERENCES user(id),
    level       INTEGER NOT NULL DEFAULT 0,
    -- 0=Unknown 1=Recognised 2=Understood
    -- 3=Applied 4=Internalised
    updated_at  TEXT    NOT NULL,
    UNIQUE(term_id, user_id)
);

CREATE TABLE IF NOT EXISTS dictionary_term_visit (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    term_id     INTEGER NOT NULL
                  REFERENCES dictionary_term(id),
    user_id     INTEGER REFERENCES user(id),
    source      TEXT,  -- search/fk/direct/repetition
    visited_at  TEXT   NOT NULL
);

Dictionary triggers (~15)

Trigger namePhaseTableOpsPurpose
term_visit_after_readAfterdictionary_termReadWrite a dictionary_term_visit record every time a term is read — tracks usage patterns
search_instead_of_listInsteadOfdictionary_termListFulltext search with 3-level relevance scoring: exact title > exact alias > prefix title > prefix alias > substring title > substring alias
alias_search_mergeInsteadOfdictionary_term_aliasListMerge alias search results with direct term results for autocomplete dropdown
term_history_after_updateAfterdictionary_termUpdateWrite before/after diff snapshot for term edits
alias_cascade_deleteBeforedictionary_termDeleteDelete all aliases, links, visits, and understanding records when a term is deleted
link_cascade_deleteBeforedictionary_termDeleteRemove all inter-term dictionary_link rows referencing the deleted term
visit_cascade_deleteBeforedictionary_termDeleteRemove all dictionary_term_visit records for the deleted term
understanding_cascade_deleteBeforedictionary_termDeleteRemove dictionary_term_understanding rows for the deleted term
annotation_cascade_deleteBeforedictionary_termDeleteRemove dictionary_annotation rows for the deleted term
url_cascade_deleteBeforedictionary_termDeleteRemove dictionary_url rows for the deleted term
understanding_upsertInsteadOfdictionary_term_understandingCreateUPSERT logic — update level if row for (term_id, user_id) already exists
visit_after_alias_resolveAfterdictionary_term_aliasReadLog visit when an alias lookup resolves to a canonical term
older_newer_term_listInsteadOfdictionary_termListReturn previous/next term by ID for OlderTerm/NewerTerm navigation in the frontend
map_cascade_deleteBeforedictionary_mapDeleteCascade-unset map_id on all terms belonging to the deleted map
source_cascade_deleteBeforedictionary_sourceDeleteRemove all dictionary_url rows linked to the deleted source

Dictionary validators

ValidatorModelOperationsRule enforced
TermOwnerValidatordictionary_termUpdate, DeleteOnly the creating user or Admin may modify or delete a term
UnderstandingLevelValidatordictionary_term_understandingCreate, UpdateLevel must be integer 0–4; user can only modify their own understanding record
AliasUniqueValidatordictionary_term_aliasCreateAlias text must be unique across all terms in the same language to prevent lookup ambiguity

Understanding level enum

// dictionary_term_understanding.level values
0 = Unknown       — term not yet studied
1 = Recognised    — can recognise when encountered
2 = Understood    — can explain the meaning
3 = Applied       — can use correctly in context
4 = Internalised  — fully automatic, no effort required
Dictionary term detail
Term detail view — Dictionary Plugin
Term list
Term list with search autocomplete

Build Your Own Plugin

The plugin system is the intended path for all domain extensions. The four built-in plugins (core, slip_box, repetition, dictionary) are the authoritative structural templates.

1

Create the module skeleton

Create src/hive-plugin-<your-domain>/ mirroring the structure of an existing plugin.

src/hive-plugin-myplugin/
├── CMakeLists.txt
├── include/hive/plugin/myplugin/
│   ├── MyPlugin.hpp
│   └── MyModel.hpp
└── src/
    ├── MyPlugin.cpp
    ├── MyValidator.cpp
    ├── MyTrigger.cpp
    └── migrations/
        ├── V1__initial_schema.sql
        └── V2__add_index.sql
2

Declare the plugin class

#include <hive/api/Plugin.hpp>

class MyPlugin : public hive::Plugin {
public:
    MyPlugin() {
        add_dependency("core");  // or "slip_box" etc.
    }

    std::string get_name() const override { return "my_plugin"; }

    void initialize(PluginContext& ctx) override {
        // Register everything here — called at startup
        register_migrations(load_sql_files("migrations/"));

        register_model(MyModel::definition());

        register_validator("my_model",
            std::make_shared<MyValidator>());

        register_trigger(std::make_shared<MyAfterCreateTrigger>());

        register_job(std::make_shared<MyCleanupJob>());
    }
};
3

Define ModelDefinition entries

static ModelDefinition definition() {
    return ModelDefinition("my_model")
        .set_group("My Domain")
        .set_title_column("name")
        .set_columns({
            ColumnDefinition("id")
                .set_flags(INTEGER | AUTO | HIDDEN),
            ColumnDefinition("name")
                .set_flags(TEXT | MANDATORY | MUTABLE),
            ColumnDefinition("description")
                .set_flags(TEXTAREA | MUTABLE),
            ColumnDefinition("owner_id")
                .set_flags(INTEGER | FOREIGN_KEY | MANDATORY)
                .set_foreign_key_model("user")
        })
        .set_all_rest_operations()  // CRUD + List
        .set_cache_enabled(true);
}
4

Wire into the build

In the root CMakeLists.txt: add add_subdirectory(src/hive-plugin-myplugin). In src/hive-app/CMakeLists.txt: link hive_plugin_myplugin to hive_app. In Main.cpp: register the factory:

#include <hive/plugin/myplugin/MyPlugin.hpp>

// In main():
plugin_registry.register_factory("my_plugin",
    []() { return std::make_unique<MyPlugin>(); });
5

Verify end-to-end

After startup, check the complete chain:

  1. Migrations applied — check migration table in SQLite
  2. GET /api/v1/model_definitionmy_model appears
  3. CRUD endpoints accessible at /api/v1/my_model
  4. Frontend renders CRUD screens automatically
  5. Validators reject invalid requests with correct status codes
  6. Triggers fire in correct order — check history table
  7. Jobs appear in job_entry table with correct cron
Full Developer Guide with Code Examples →

Ready to extend Hive with your domain?

The Developer Guide provides complete C++ code examples for validators, triggers, jobs, and the ModelDefinition fluent builder API.