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.
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 method | What it registers | Used 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) |
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)
core ◄── slip_box ◄── repetition core ◄── dictionary
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/.
| Table | Purpose |
|---|---|
user | User accounts — username, password hash, role, status, registration date |
access_token | Named persistent API tokens with expiry and revocation |
refresh_token | Session refresh tokens — hashed, rotating |
login_session | Tracked login sessions — IP, user agent, expiry |
auth_log | Authentication events — login success/failure, logout, token refresh |
api_log | All API requests — method, path, status, duration, user |
super_admin_log | Privileged operations log — only accessible to SuperAdmin |
history | CRUD operation history for all models (written by global After trigger) |
error | Server-side error tracking — type, message, stack, timestamp |
alert | System alerts — severity, message, acknowledged flag |
job_entry | Persisted job definitions — name, cron, enabled, last_run |
job_run | Job execution telemetry — start, end, success, output string |
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
};
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.
-- 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
);
| Trigger name | Phase | Table | Operations | Purpose |
|---|---|---|---|---|
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 |
| Job class | Default schedule | Purpose |
|---|---|---|
DatabaseVacuumJob | 0 2 * * 0 (Sun 02:00) | SQLite VACUUM — compact the database file and reclaim fragmented space |
AuthLogCleanupJob | 0 3 * * * (03:00 daily) | Purge auth_log entries older than retention period |
ApiLogCleanupJob | 0 3 * * * (03:00 daily) | Purge api_log entries older than retention period |
HistoryCleanupJob | 0 4 * * * (04:00 daily) | Prune old records from the history table |
ErrorCleanupJob | 0 4 * * * (04:00 daily) | Purge old entries from the error table |
SuperAdminLogCleanupJob | 0 5 * * 0 (Sun 05:00) | Purge old entries from super_admin_log |
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/.
| Table | Purpose |
|---|---|
note | Primary knowledge unit — title, content (Markdown), parent, map |
map | Logical grouping container for notes |
link | Directional connections between notes (source_id → target_id, type) |
tag | Tag definitions |
note_tag | Note ↔ tag many-to-many junction |
flag | Note flagging (importance, review needed, etc.) |
pinned_note | Per-user pinned notes list |
concept | Title / disambiguation entity with note linkage |
source | Book / web references (title, URL, author, year) |
wanted_note | Placeholder for planned-but-not-yet-written notes |
idea | Quick idea capture (fleeting notes) |
project | Project entity for structured work tracking |
task | Task entity linked to a project |
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
);
The Slipbox plugin registers these static JS/CSS files for the frontend note editor:
markdown-it.min.js — Markdown renderermarkdown-it-emoji.min.js — emoji extensionhighlight.min.js — syntax highlightinggithub.min.css — code highlight theme| Trigger name | Phase | Table | Ops | Purpose |
|---|---|---|---|---|
note_path_before_create | Before | note | Create | Compute and set the path field (ancestor chain) before persisting a new note |
note_path_before_update | Before | note | Update | Recompute path if parent_id or map_id changed |
note_children_path_after_update | After | note | Update | Recursively update path on all child notes when parent path changes |
note_history_after_create | After | note | Create | Write initial snapshot to note history table |
note_history_after_update | After | note | Update | Write before/after diff to note history |
concept_alias_resolve | InsteadOf | concept | List | Resolve concept aliases during search — merge alias matches with direct concept matches |
wanted_to_note_trigger | After | note | Create | If new note title matches a wanted_note entry, automatically link/resolve it |
link_cascade_delete | Before | note | Delete | Remove all outbound and inbound links when a note is deleted |
tag_cleanup | After | note | Delete | Orphan note_tag junction rows after note deletion |
search_instead_of_list | InsteadOf | note | List | Override list with fulltext LIKE search when q query param is present |
| Query name | Purpose |
|---|---|
get_children | Return direct child notes of a given parent_id |
get_path_notes | Return the full ancestor chain (breadcrumb) for a note |
get_linked_notes | Return all notes linked from/to a given note ID |
get_notes_by_tag | Return notes with a specific tag ID |
| Job class | Default schedule | Purpose |
|---|---|---|
HtmlExportJob | Configurable | Exports 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. |
SuperMemo-style spaced repetition system for systematic knowledge review and memorisation scheduling. Depends on: slip_box (and transitively core). Source: src/hive-plugin-repetition/.
| Table | Purpose |
|---|---|
repetition_session | A review session — user, start time, status (active/completed), item count |
repetition_review | Individual card review — session, note_id, rating (0–5), timestamp |
repetition_prediction | Next-review scheduling per note — interval, ease factor, due date |
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
);
| Trigger | Phase | Table | Op | Purpose |
|---|---|---|---|---|
review_after_create | After | repetition_review | Create | After each card rating, recompute repetition_prediction: interval, ease factor, next due date using SM-2 algorithm |
session_complete_after_update | After | repetition_session | Update | When a session status changes to completed, set completed_at timestamp |
note_delete_cascade | Before | repetition_prediction | Delete | Clean up reviews and predictions when source note is deleted from Slipbox |
due_items_instead_of_list | InsteadOf | repetition_prediction | List | Override standard list to filter by due date, with support for due/not-due/never-reviewed variants via query param |
| Query name | Purpose |
|---|---|
get_due_notes | Return notes due for review (due_date ≤ today), with note title from Slipbox join |
get_session_stats | Return counts: due, not-due, never-reviewed per user |
/web/app_repetition.htmlA 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/.
| Table | Purpose |
|---|---|
dictionary_map | Thematic map — grouping container with emoji, name, description |
dictionary_term | Primary term / word — title, definition (Markdown), map_id, language |
dictionary_term_alias | Alternate names / spellings for a term |
dictionary_source | Reference sources — book, web URL, author, year |
dictionary_link | Directional inter-term links — source term → target term, relation type |
dictionary_url | External URL references attached to a term |
dictionary_term_visit | Visit history per term — user, timestamp, source (search, FK, direct) |
dictionary_term_understanding | Per-user understanding level per term (0=Unknown to 4=Internalised) |
dictionary_annotation | Personal annotations on terms — user-specific Markdown notes |
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
);
| Trigger name | Phase | Table | Ops | Purpose |
|---|---|---|---|---|
term_visit_after_read | After | dictionary_term | Read | Write a dictionary_term_visit record every time a term is read — tracks usage patterns |
search_instead_of_list | InsteadOf | dictionary_term | List | Fulltext search with 3-level relevance scoring: exact title > exact alias > prefix title > prefix alias > substring title > substring alias |
alias_search_merge | InsteadOf | dictionary_term_alias | List | Merge alias search results with direct term results for autocomplete dropdown |
term_history_after_update | After | dictionary_term | Update | Write before/after diff snapshot for term edits |
alias_cascade_delete | Before | dictionary_term | Delete | Delete all aliases, links, visits, and understanding records when a term is deleted |
link_cascade_delete | Before | dictionary_term | Delete | Remove all inter-term dictionary_link rows referencing the deleted term |
visit_cascade_delete | Before | dictionary_term | Delete | Remove all dictionary_term_visit records for the deleted term |
understanding_cascade_delete | Before | dictionary_term | Delete | Remove dictionary_term_understanding rows for the deleted term |
annotation_cascade_delete | Before | dictionary_term | Delete | Remove dictionary_annotation rows for the deleted term |
url_cascade_delete | Before | dictionary_term | Delete | Remove dictionary_url rows for the deleted term |
understanding_upsert | InsteadOf | dictionary_term_understanding | Create | UPSERT logic — update level if row for (term_id, user_id) already exists |
visit_after_alias_resolve | After | dictionary_term_alias | Read | Log visit when an alias lookup resolves to a canonical term |
older_newer_term_list | InsteadOf | dictionary_term | List | Return previous/next term by ID for OlderTerm/NewerTerm navigation in the frontend |
map_cascade_delete | Before | dictionary_map | Delete | Cascade-unset map_id on all terms belonging to the deleted map |
source_cascade_delete | Before | dictionary_source | Delete | Remove all dictionary_url rows linked to the deleted source |
| Validator | Model | Operations | Rule enforced |
|---|---|---|---|
TermOwnerValidator | dictionary_term | Update, Delete | Only the creating user or Admin may modify or delete a term |
UnderstandingLevelValidator | dictionary_term_understanding | Create, Update | Level must be integer 0–4; user can only modify their own understanding record |
AliasUniqueValidator | dictionary_term_alias | Create | Alias text must be unique across all terms in the same language to prevent lookup ambiguity |
// 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
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.
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
#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>());
}
};
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);
}
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>(); });
After startup, check the complete chain:
GET /api/v1/model_definition — my_model appears/api/v1/my_modelhistory tablejob_entry table with correct cron