A complete walkthrough — from an empty directory to a production-safe plugin with models, migrations, validators, triggers, and scheduled jobs. All examples use real Hive API types from include/hive-api/ and include/hive-model/.
Hive enforces strict module boundaries. Violating them causes build failures (dependency order enforced by CMake). Before writing any code, understand which module your code belongs in.
| Module | Include this for | Do NOT include from |
|---|---|---|
hive-api |
Plugin base class, IValidator, IRepository, Trigger, Job, JobConfig, OperationResult — everything a plugin author needs | Never include hive-http, hive-essential, or hive-db-sqlite from plugin code |
hive-model |
ModelDefinition, ColumnDefinition, ColumnDefinitionFlag, Crudl, TriggerPhase, AccessMode enums | Does not depend on any other Hive module — pure metadata types |
hive-orm |
BaseModel entity class, column constant structs (BaseColumns-style typed access) | Should not include hive-db-sqlite or hive-http |
hive-plugin-* |
Include hive-api and hive-model only. Never directly call hive-db-sqlite or hive-http. | All database access must go through IRepository. All HTTP routing through ModelDefinition registration. |
Every entity you want Hive to manage must be described by a ModelDefinition. This single struct drives REST routing, database persistence, frontend rendering, caching, and validation — simultaneously.
#include <hive/model/ModelDefinition.hpp>
#include <hive/model/ColumnDefinition.hpp>
// All ColumnDefinitionFlag values (bitmask integers)
using namespace hive::model::flags;
// MANDATORY=1 UNIQUE=2 FOREIGN_KEY=4 AUTO=8
// HIDDEN=16 READONLY=32 MUTABLE=64 INTERNAL=128
// TEXT=256 TEXTAREA=512 INTEGER=1024 REAL=2048
// BLOB=4096 BOOL=8192 DATETIME=16384
static hive::ModelDefinition book_definition() {
return hive::ModelDefinition("book")
// Grouping in the frontend navigation sidebar
.set_group("Library")
// Which column provides the human-readable name for this entity
// Used by FK dropdowns in other models that reference "book"
.set_title_column("title")
// Enable all 5 CRUD operations + List
.set_all_rest_operations()
// Or selectively: .set_rest_operations({Crudl::Read, Crudl::List})
// Column definitions
.set_columns({
// Primary key — always INTEGER | AUTO | HIDDEN
ColumnDefinition("id")
.set_flags(INTEGER | AUTO | HIDDEN),
// Mandatory text field — shown in list and forms
ColumnDefinition("title")
.set_flags(TEXT | MANDATORY | MUTABLE),
// Optional multi-line text (Markdown editor in frontend)
ColumnDefinition("description")
.set_flags(TEXTAREA | MUTABLE),
// Integer field — optional
ColumnDefinition("page_count")
.set_flags(INTEGER | MUTABLE),
// Real (floating point)
ColumnDefinition("rating")
.set_flags(REAL | MUTABLE),
// Boolean checkbox
ColumnDefinition("is_read")
.set_flags(BOOL | MUTABLE),
// Foreign key — renders as <select> dropdown in frontend
// Populated by list_all_entities("author") call
ColumnDefinition("author_id")
.set_flags(INTEGER | FOREIGN_KEY | MANDATORY)
.set_foreign_key_model("author"),
// UNIQUE constraint in the DB
ColumnDefinition("isbn")
.set_flags(TEXT | UNIQUE | MUTABLE),
// Auto-set server-side — shown read-only, excluded from forms
ColumnDefinition("created_at")
.set_flags(DATETIME | AUTO | READONLY),
ColumnDefinition("updated_at")
.set_flags(DATETIME | AUTO | READONLY),
// INTERNAL — never sent to client at all
ColumnDefinition("internal_score")
.set_flags(INTEGER | INTERNAL),
// HIDDEN — stored and used server-side but not shown in UI
ColumnDefinition("user_id")
.set_flags(INTEGER | FOREIGN_KEY | HIDDEN)
.set_foreign_key_model("user")
})
// Enable LRU+TTL cache for read and list operations
.set_cache_enabled(true)
// Also cache immediately after create (pre-populate cache)
.set_cached_after_create(true)
// Custom action buttons shown in the read view
.add_custom_read_action("export_pdf", "Export as PDF")
.add_custom_read_action("send_email", "Email to user")
// Custom action for the list view
.add_custom_list_action("import_csv", "Import from CSV")
// Mark entire model as read-only (no create/update/delete by anyone)
// .set_readonly(true)
// Allow users with role Reader (normally read-only) to also write
// .allow_reader_write(true)
// Virtual table — no DB table, data comes from InsteadOf triggers
// .set_virtual_table(true)
// No table at all — metadata-only model (used for custom API surfaces)
// .set_no_table(true)
;
}
When you set a type flag, related flags are inferred automatically. You can always override by explicitly setting or clearing flags:
| If flag is set | Auto-inferred | Reason |
|---|---|---|
AUTO | Also READONLY | Auto-generated fields (PK, timestamps) should never appear in input forms |
FOREIGN_KEY | Also INTEGER | FKs are always integer references to other model IDs |
MANDATORY (without AUTO) | Also MUTABLE | A mandatory field that's not auto-set must be editable |
UNIQUE | Also MUTABLE (if not AUTO) | Unique user-provided fields need to be editable |
INTERNAL | Also HIDDEN | Internal fields are never sent to the client — HIDDEN is the weaker subset |
Validators are the security and consistency gate of Hive. They run before any trigger or persistence call. A validator can inspect the AccessTokenContext (who is calling, with what role) and the request payload, and either pass or reject the operation.
#include <hive/api/OperationResult.hpp>
struct OperationResult {
int status; // 0 or 200 = ok; any other = HTTP error code
std::string error; // human-readable message returned to client
bool ok() const { return status == 0 || status == 200; }
};
// Built-in result macros (use these instead of raw constructors)
ok_result // {0, ""}
status_403_forbidden // {403, "Forbidden"}
status_405_unsupported_operation // {405, "Unsupported operation"}
#include <hive/api/IValidator.hpp>
#include <hive/api/OperationResult.hpp>
#include <hive/api/AccessTokenContext.hpp>
class BookOwnerValidator : public hive::IValidator {
public:
// Called before every Create operation on the "book" model
OperationResult can_create(
hive::AccessTokenContext& ctx,
hive::Fields& fields
) override {
// Guest users cannot create books
if (ctx.role == hive::UserRole::Guest) {
return status_403_forbidden;
}
// Inject the current user's ID as owner
fields["user_id"] = std::to_string(ctx.user_id);
return ok_result;
}
// Called before every Read operation
OperationResult can_read(
hive::AccessTokenContext& ctx,
int id
) override {
// All authenticated users can read — Guests cannot
if (ctx.role == hive::UserRole::Guest) {
return {403, "Login required to read books."};
}
return ok_result;
}
// Called before every Update operation
OperationResult can_update(
hive::AccessTokenContext& ctx,
int id,
hive::Fields& fields
) override {
// Admin can update anything
if (ctx.role >= hive::UserRole::Admin) {
return ok_result;
}
// Other users can only update their own books
// Use a query to check ownership
auto book = book_query_.find_by_id(id);
if (!book || book->user_id != ctx.user_id) {
return {403, "You can only edit your own books."};
}
return ok_result;
}
// Called before every Delete operation
OperationResult can_delete(
hive::AccessTokenContext& ctx,
int id
) override {
if (ctx.role < hive::UserRole::Editor) {
return status_403_forbidden;
}
return ok_result;
}
// Called before every List operation
OperationResult can_list(
hive::AccessTokenContext& ctx,
hive::QueryParams& qp
) override {
// For non-admin users, silently filter list to their own books
if (ctx.role < hive::UserRole::Admin) {
qp.add_filter("user_id", std::to_string(ctx.user_id));
}
return ok_result;
}
private:
BookQueryHelper book_query_; // injected via constructor in real code
};
// In MyPlugin::initialize(PluginContext& ctx)
register_validator("book", std::make_shared<BookOwnerValidator>());
// Multiple validators can be registered for the same model
// They execute in registration order — first rejection wins
register_validator("book", std::make_shared<BookMandatoryFieldsValidator>());
register_validator("book", std::make_shared<BookIsbnFormatValidator>());
Triggers hook into the CRUD pipeline at three phases. Unlike validators (which only accept/reject), triggers can modify data, replace persistence entirely, or execute side effects after persistence.
| TriggerPhase | Value | When it fires | Can modify data? | Can abort? |
|---|---|---|---|---|
Before | 0 | After validators, before repository call | Yes — return modified Fields | Yes — return nullopt |
After | 1 | After repository call, post-persistence | No (entity already written) | No (cannot undo) |
InsteadOf | 2 | Replaces the repository call entirely | Yes — full control | By returning nullopt |
Around | 3 | Reserved for future use | — | — |
#include <hive/api/Trigger.hpp>
// Trigger(name, priority, operations, phase, table)
// table = "*" means all registered models
// table = "book" means only the "book" model
// Lower priority integer = earlier execution
// Example: a global After-trigger that logs all creates to "history"
class GlobalHistoryTrigger : public hive::Trigger {
public:
GlobalHistoryTrigger() : hive::Trigger(
"global_history", // name
100, // priority (high = late = after model triggers)
{hive::Crudl::Create,
hive::Crudl::Update,
hive::Crudl::Delete}, // operations to fire for
hive::TriggerPhase::After,
"*" // all models
) {}
// After phase: run_before_or_after is called for Before AND After
std::optional<hive::Fields> run_before_or_after(
hive::AccessTokenContext& ctx,
hive::Fields& fields
) override {
auto history_repo = get_repository("history");
hive::Fields hist;
hist["model_name"] = fields["_model_name"];
hist["entity_id"] = fields["id"];
hist["operation"] = fields["_operation"];
hist["user_id"] = std::to_string(ctx.user_id);
hist["after_data"] = fields_to_json(fields);
std::string err;
history_repo->create(hist, err);
return fields; // return original fields unmodified
}
};
class BookTimestampBeforeTrigger : public hive::Trigger {
public:
BookTimestampBeforeTrigger() : hive::Trigger(
"book_timestamp_before",
10,
{hive::Crudl::Create, hive::Crudl::Update},
hive::TriggerPhase::Before,
"book"
) {}
std::optional<hive::Fields> run_before_or_after(
hive::AccessTokenContext& ctx,
hive::Fields& fields
) override {
auto now = current_iso8601_timestamp();
if (fields["_operation"] == "create") {
fields["created_at"] = now;
}
fields["updated_at"] = now;
return fields; // modified fields get written to DB
// To abort the operation: return std::nullopt;
}
};
class BookSearchInsteadOfTrigger : public hive::Trigger {
public:
BookSearchInsteadOfTrigger() : hive::Trigger(
"book_search",
1,
{hive::Crudl::List},
hive::TriggerPhase::InsteadOf,
"book"
) {}
// InsteadOf List — fired when "q" query param is present
std::optional<hive::Entities> run_instead_of_list(
hive::AccessTokenContext& ctx,
hive::QueryParams& qp
) override {
if (!qp.has("q")) {
return std::nullopt; // Fall through to standard list
}
std::string query = qp.get("q");
auto repo = get_repository("book");
std::string err;
hive::QueryParams search_qp;
search_qp.set_raw_sql(
"WHERE title LIKE ? OR description LIKE ?",
{"%" + query + "%", "%" + query + "%"}
);
return repo->list(search_qp, err);
}
};
// In MyPlugin::initialize(PluginContext& ctx)
register_trigger(std::make_shared<BookTimestampBeforeTrigger>());
register_trigger(std::make_shared<BookSearchInsteadOfTrigger>());
register_trigger(std::make_shared<GlobalHistoryTrigger>());
// Trigger execution order within a phase is determined by priority:
// Lower integer = earlier execution
// Priority 1 → fires first
// Priority 100 → fires last (after model-specific triggers)
Jobs run on a fixed schedule managed by CronScheduler (4 worker threads). Each job run is persisted to the job_run table. Jobs can access repositories and queries via JobConfig.
#include <hive/api/Job.hpp>
#include <hive/api/JobConfig.hpp>
class BookWeeklyReportJob : public hive::Job {
public:
BookWeeklyReportJob() : hive::Job(
"book_weekly_report", // unique name (stored in job_entry table)
"Weekly reading report", // human-readable description
"0 9 * * 1", // cron: every Monday at 09:00
true, // enabled_by_default
true // run_once_when_missed
) {}
// run() is called by a CronScheduler worker thread
// Return value: string written to job_run.output
std::string run(hive::JobConfig& config) override {
auto book_repo = config.get_repository("book");
std::string err;
hive::QueryParams qp;
qp.set_filter("created_at", last_7_days_filter());
auto recent_books = book_repo->list(qp, err);
if (!err.empty()) {
return "ERROR: " + err;
}
auto stats = config.call_query("book_stats", {});
std::ostringstream report;
report << "Weekly book report:\n";
report << " New books this week: " << recent_books.size() << "\n";
report << " Total books: " << stats["total_count"] << "\n";
report << " Avg rating: " << stats["avg_rating"] << "\n";
return report.str();
}
};
class JobConfig {
public:
std::shared_ptr<IRepository> get_repository(const std::string& model_name);
hive::QueryResult call_query(
const std::string& name,
const std::vector<std::string>& args
);
const PluginRegistry& get_plugin_registry() const;
};
"0 2 * * 0" // Every Sunday at 02:00 (weekly vacuum)
"0 3 * * *" // Every day at 03:00 (daily cleanup)
"*/15 * * * *" // Every 15 minutes
"0 9 * * 1" // Every Monday at 09:00
"0 0 1 * *" // First day of every month at midnight
Hive uses versioned SQL migration files with SHA-256 chain-hash integrity. The integrity system guarantees that applied migrations are never silently modified after deployment.
V{sequence_number}__{descriptive_name}.sql
Regex: V(\d+)__([a-zA-Z0-9_]+).sql
# Valid:
V1__initial_schema.sql
V2__add_user_id_to_book.sql
V3__add_isbn_index.sql
V10__rename_description_to_summary.sql
# INVALID (will fail validation):
v1_initial_schema.sql # lowercase v
V1_initial_schema.sql # single underscore
V1__initial schema.sql # space in name
-- V1__initial_schema.sql
CREATE TABLE IF NOT EXISTS book (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
rating REAL,
is_read INTEGER NOT NULL DEFAULT 0,
isbn TEXT UNIQUE,
created_at TEXT NOT NULL,
updated_at TEXT,
user_id INTEGER REFERENCES user(id)
);
-- V2__add_isbn_index.sql
CREATE INDEX IF NOT EXISTS idx_book_isbn ON book(isbn);
-- V3__add_genre_column.sql
ALTER TABLE book ADD COLUMN genre TEXT;
// For each migration file in sequence order:
// file_hash = SHA-256(file_content)
// chain_hash = SHA-256(previous_chain_hash || file_hash)
// Store: (filename, file_hash, chain_hash, applied_at)
//
// On subsequent startups:
// Recompute file_hash from current file
// If file_hash != stored: FATAL — migration file was tampered
// Recompute chain_hash: if differs: FATAL — ordering attack detected
//
// Golden rule: NEVER edit a migration after it has been applied.
// Add a new migration file instead.
// In MyPlugin::initialize(PluginContext& ctx)
register_migrations({
hive::Migration("V1__initial_schema.sql",
R"SQL(
CREATE TABLE IF NOT EXISTS book (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_at TEXT NOT NULL
);
)SQL"
),
hive::Migration("V2__add_description.sql",
"ALTER TABLE book ADD COLUMN description TEXT;"
),
});
#pragma once
#include <hive/api/Plugin.hpp>
class MyPlugin : public hive::Plugin {
public:
MyPlugin();
std::string get_name() const override { return "my_plugin"; }
void initialize(hive::PluginContext& ctx) override;
};
#include "MyPlugin.hpp"
#include "BookValidator.hpp"
#include "BookTimestampTrigger.hpp"
#include "BookSearchTrigger.hpp"
#include "BookStatsQuery.hpp"
#include "BookWeeklyReportJob.hpp"
#include <hive/model/ModelDefinition.hpp>
#include <hive/model/ColumnDefinition.hpp>
using namespace hive::model::flags;
MyPlugin::MyPlugin() {
add_dependency("core"); // required — always load core first
}
void MyPlugin::initialize(hive::PluginContext& ctx) {
// 1. Migrations — applied first, in order, with chain-hash integrity
register_migrations({
hive::Migration("V1__initial_schema.sql",
R"SQL(
CREATE TABLE IF NOT EXISTS book (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
rating REAL,
is_read INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT,
user_id INTEGER REFERENCES user(id)
);
)SQL"),
hive::Migration("V2__add_isbn.sql",
"ALTER TABLE book ADD COLUMN isbn TEXT UNIQUE;"),
});
// 2. Model definitions — drive REST routing and frontend
register_model(
hive::ModelDefinition("book")
.set_group("Library")
.set_title_column("title")
.set_all_rest_operations()
.set_columns({
ColumnDefinition("id")
.set_flags(INTEGER | AUTO | HIDDEN),
ColumnDefinition("title")
.set_flags(TEXT | MANDATORY | MUTABLE),
ColumnDefinition("description")
.set_flags(TEXTAREA | MUTABLE),
ColumnDefinition("rating")
.set_flags(REAL | MUTABLE),
ColumnDefinition("is_read")
.set_flags(BOOL | MUTABLE),
ColumnDefinition("isbn")
.set_flags(TEXT | UNIQUE | MUTABLE),
ColumnDefinition("created_at")
.set_flags(DATETIME | AUTO | READONLY),
ColumnDefinition("updated_at")
.set_flags(DATETIME | AUTO | READONLY),
ColumnDefinition("user_id")
.set_flags(INTEGER | FOREIGN_KEY | HIDDEN)
.set_foreign_key_model("user"),
})
.set_cache_enabled(true)
.add_custom_list_action("export_csv", "Export CSV")
);
// 3. Validators
register_validator("book", std::make_shared<BookOwnerValidator>());
// 4. Triggers
register_trigger(std::make_shared<BookTimestampBeforeTrigger>());
register_trigger(std::make_shared<BookSearchInsteadOfTrigger>());
// 5. Custom queries
register_query("book_stats", std::make_shared<BookStatsQuery>());
// 6. Jobs
register_job(std::make_shared<BookWeeklyReportJob>());
// close_for_changes() is called automatically after initialize()
// — the plugin registry becomes read-only at runtime
}
add_library(hive_plugin_my_plugin
src/MyPlugin.cpp
src/BookValidator.cpp
src/BookTimestampTrigger.cpp
src/BookSearchTrigger.cpp
src/BookStatsQuery.cpp
src/BookWeeklyReportJob.cpp
)
target_include_directories(hive_plugin_my_plugin PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(hive_plugin_my_plugin
hive_api # Plugin, IValidator, Trigger, Job, OperationResult
hive_model # ModelDefinition, ColumnDefinition, enums
)
#include <hive/plugin/myplugin/MyPlugin.hpp>
// In main():
plugin_registry.register_factory("my_plugin",
[]() -> std::unique_ptr<hive::Plugin> {
return std::make_unique<MyPlugin>();
}
);
When a plugin isn't working as expected, work through this checklist in order:
Check server startup logs for: [INFO] Plugin loaded: my_plugin
If missing: verify allowed_plugins=my_plugin in hive.properties, factory registration in Main.cpp, and get_name() returns exactly "my_plugin".
Check startup logs for migration output. If integrity error: revert the edited file or recreate the DB. If migration not found: verify the V{n}__name.sql naming — double underscore required.
sqlite3 hive.db \
"SELECT * FROM hive_migrations WHERE plugin='my_plugin';"
curl -s http://localhost:9000/api/v1/model_definition | \
jq '.[] | select(.name == "book")'
If missing: check register_model() call in initialize(). Verify set_all_rest_operations() or explicit ops are set.
TOKEN="eyJhbG..." # from login response
curl -s http://localhost:9000/api/v1/book \
-H "Authorization: Bearer $TOKEN"
curl -s -X POST http://localhost:9000/api/v1/book \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"Test Book"}'
403 = validator rejecting. 405 = operation not enabled in ModelDefinition. 500 = server error — check logs.
After a create: check the history table. Check created_at / updated_at fields. If Before trigger not modifying fields: verify it returns fields (not std::nullopt).
sqlite3 hive.db \
"SELECT * FROM history WHERE model_name='book' ORDER BY id DESC LIMIT 5;"
sqlite3 hive.db \
"SELECT * FROM job_entry WHERE name='book_weekly_report';"
sqlite3 hive.db \
"SELECT * FROM job_run ORDER BY id DESC LIMIT 5;"
If job not in job_entry: check register_job(). If runs fail: check job_run.output for the error string. If never runs: validate the cron expression.
# configuration/hive.properties
max_log_level=DEBUG
# Or on command line:
./hive_app start --log-level DEBUG ...
Debug output shows every request step: auth context resolution, validator name + result, trigger name + phase + outcome, SQL queries, and cache hit/miss statistics.
All types a plugin author needs are in include/hive-api/ and include/hive-model/.
| Header | Key types / constants |
|---|---|
hive/api/Plugin.hpp | Plugin — register_model, register_validator, register_trigger, register_job, register_query, register_migrations, add_dependency, close_for_changes |
hive/api/IValidator.hpp | IValidator — can_create, can_read, can_update, can_delete, can_list |
hive/api/IRepository.hpp | IRepository — create, read, update, remove, list, list_in_ids, list_ids, request_to_entity_fields |
hive/api/Trigger.hpp | Trigger — run_before_or_after, run_instead_of_create/read/update/delete/list |
hive/api/Job.hpp | Job — run(JobConfig&) → string |
hive/api/JobConfig.hpp | JobConfig — get_repository, call_query, get_plugin_registry |
hive/api/OperationResult.hpp | OperationResult {status, error}, macros ok_result, status_403_forbidden, status_405_unsupported_operation |
hive/model/ModelDefinition.hpp | ModelDefinition — full fluent builder API including all set_*() and add_custom_*_action() methods |
hive/model/ColumnDefinition.hpp | ColumnDefinition — set_flags, set_foreign_key_model; all ColumnDefinitionFlag constants |
hive/model/Crudl.hpp | Crudl enum: Undefined=0 Create=1 Read=2 Update=3 Delete=4 List=5 |
hive/model/TriggerPhase.hpp | TriggerPhase: Before=0 After=1 InsteadOf=2 Around=3 |
hive/model/AccessMode.hpp | AccessMode (0–8): MaintenanceMode … PublicFullAccess |
hive/model/UserRole.hpp | UserRole: Guest=0 Reader=1 Editor=2 Reviewer=3 Admin=4 SuperAdmin=5 System=100 |