Building a Hive Plugin

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/.

Module Boundaries — What Belongs Where

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.

ModuleInclude this forDo NOT include from
hive-api Plugin base class, PluginFactory macros, IValidator, IRepository, Trigger, TriggerPhase, Job, JobConfig, OperationResult, AccessTokenContext — everything a plugin author needs Never include hive-http or hive-db-sqlite internals from plugin code
hive-model ModelDefinition, ColumnDefinition, ColumnDefinitionFlag, EnumDefinition — pure metadata types Does not depend on HTTP or persistence modules
hive-essential Shared enums and runtime services: Crudl, UserRole, UserStatus, AccessMode, RegistrationMode, DatabaseType, Configuration
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.

ModelDefinition — Fluent Builder API

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.

Real ModelDefinition example — dictionary_term

This is the actual (abridged) definition from include/hive-plugin-dictionary/.../models/DictionaryTerm.hppdef is an alias for ModelDefinition, coldef for ColumnDefinition:

// ColumnDefinitionFlag bitmask values (ColumnDefinition.hpp):
// 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

inline const def DICTIONARY_TERM_DEFINITION =
    def(COLS::MODEL_NAME, "dictionary")          // (model name, plugin name)
    .set_group("Dictionary", 200)                // frontend nav group + order
    .set_all_rest_operations()                   // enable C+R+U+D+L
    .set_title_column(COLS::TITLE)               // display column for FK labels
    .set_columns({
        // id, created_at, updated_at are prepended automatically
        coldef(COLS::DICTIONARY_MAP_ID, MANDATORY | FOREIGN_KEY | READONLY)
            .set_description("Dictionary map this term belongs to."),
        coldef(COLS::TITLE, MANDATORY)
            .set_description("Title of the dictionary term."),
        coldef(COLS::DISAMBIGUATION),
        coldef(COLS::DEFINITION, TEXTAREA)
            .set_description("Definition of the term."),
        coldef(COLS::STATUS)
            .set_default_value(0)
            .set_enum_definition(enums::term_status_to_enum_definition())
            .set_description("Status of the term."),
        coldef(COLS::IMPORTANCE)
            .set_default_value(2)
            .set_enum_definition(enums::importance_to_enum_definition()),
        coldef(COLS::DIFFICULTY)
            .set_default_value(2)
            .set_enum_definition(enums::difficulty_to_enum_definition()),
        coldef(COLS::IS_FOR_REPETITION, BOOL).set_default_value(false),
        coldef(COLS::IS_ROOT, BOOL).set_default_value(false),
    })
    // Custom actions: navigation buttons in the generated UI —
    // params support the "{id}" placeholder for the current record
    .add_custom_list_action("dictionary_term_visit", "List term visits",
                            {"dictionary_term_id", "{id}"})
    .add_custom_create_action("dictionary_tag", "Add tag",
                              {"dictionary_term_id", "{id}"})
    .add_custom_read_action("dictionary_older_term", "Read older term",
                            {"id", "{id}"})
    .add_custom_read_action("dictionary_newer_term", "Read newer term",
                            {"id", "{id}"});

Other builder options

MethodEffect
set_rest_operations("crl")Enable only some operations — each char maps to Create/Read/Update/Delete/List
set_readonly()No mutations allowed via API
set_virtual_table(true)Model has no real DB table — data comes from InsteadOf triggers (e.g. note_navigation, dictionary_term_metric)
set_no_table(true)Model without any table at all
set_cache_enabled(false)Disable the LRU cache for this model (done for high-write models like api_log, history)
set_cached_after_create(false)Skip pre-populating the cache on create

Column conventions

ConventionBehaviour
set_columns()Automatically prepends id, created_at, updated_at as the first three columns
*_id suffixAutomatically typed as Integer and linked as a foreign key to the model named by the prefix
set_enum_definition(...)Binds an EnumDefinition so the API/UI shows human-readable labels instead of raw integers
set_default_value(...) / set_description(...)Default value for create; description surfaced in the generated UI

IValidator — Security and Invariant Gate

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.

OperationResult — the return type

#include <hive/api/OperationResult.hpp>

struct OperationResult {
    int         status;    // 0 = OK, any other number = HTTP error code
    std::string error;     // error description, empty if OK

    bool ok() const { return status == 0; }
    bool ko() const { return !ok(); }
    explicit operator bool() const noexcept;
};

// Built-in result macros (use these instead of raw constructors)
ok_result                          // {0, ""}
status_403_forbidden               // {403, "You are not authorized..."}
status_405_unsupported_operation   // {405, "Unsupported operation"}

The raw IValidator interface

#include <hive/api/IValidator.hpp>

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;
};

can_update receives both the new and the old entity fields, enabling diff-based validation. AccessTokenContext provides ok() (authenticated?) and user_id for ownership checks.

Real-world validator (typed layer) — DictionaryTermValidator

Domain plugins usually implement validators on a typed layer above IValidator: authorization hooks receive a RequestContext (with resolved role) and typed model instances instead of raw field maps. Real excerpt:

OperationResult DictionaryTermValidator::validate_create_authorization(
    const RequestContext& ctx, const Model& entity) const
{
    return_if(ctx.role < hive::essential::UserRole::Editor,
              403, "You can not create Terms.")

    if (!dictionary::has_right_for_map(ctx, entity.dictionary_map_id,
                                       plugins::core::enums::SingleRight::Write))
    {
        return {403, "You do not have permission to create a Term for this map."};
    }
    return ok_result;
}

OperationResult DictionaryTermValidator::validate_update_authorization(
    const RequestContext& ctx,
    const Model& old_entity, const Model& new_entity) const
{
    return_if(ctx.role < hive::essential::UserRole::Editor,
              403, "You can not update Terms.")

    if (!dictionary::has_right_for_map(ctx, new_entity.dictionary_map_id,
                                       plugins::core::enums::SingleRight::Write))
    {
        return {403, "You do not have permission to update a Term for this map."};
    }
    return ok_result;
}

Note the layered access control: the role hierarchy check (ctx.role < Editor) composes with per-map rights (has_right_for_map with SingleRight::{Read, Write, Delete}) built on the core plugin's team/team_member models — on top of the global AccessMode gate.

Registering validators

// Validators are attached at model registration — one validator per model.
// The REGISTER_MODEL macro expands to:
plugin->register_model(
    models::DICTIONARY_TERM_DEFINITION,
    std::make_shared<validators::DictionaryTermValidator>(),
    repository_factory);

Trigger — Before, After, InsteadOf, and Around

Triggers hook into the CRUD pipeline. Unlike validators (which only accept/reject), triggers can modify data, replace persistence entirely, or execute side effects after persistence.

Trigger phases and their contract

TriggerPhaseValueWhen it firesCan modify data?Can abort?
Before0After validators, before repository callYes — mutate the incoming fieldsYes — via a non-OK result
After1After repository call, post-persistenceNo (entity already written)No (cannot undo)
InsteadOf2Replaces the repository call entirelyYes — full controlReturning std::nullopt falls through to the default behaviour
Around3Like Before and After combined in one triggerYesYes (Before part)

Trigger constructor (real signature)

#include <hive/api/Trigger.hpp>

class Trigger : public AbstractTriggerJob {
  Trigger(
      const std::string& name,
      const std::string& description,
      int priority,                              // lower = runs first
      std::set<hive::essential::Crudl> operations,
      TriggerPhase phase,
      const std::string& table                   // "*" = all tables
  );
};

// Before/After handler — receives the full operation context:
run_before_or_after(
    operation,          // Crudl
    stack_depth,        // recursion guard for nested trigger calls
    validation_result,
    action_result,      // out param — non-OK aborts (Before phase)
    def,                // ModelDefinition of the target model
    user_id, id,
    fields,             // mutable entity fields
    old_fields,         // previous values (for Update)
    query_params);

// InsteadOf handlers return std::optional — empty means
// "fall through to the standard repository call":
std::optional<pair<int, OperationResult>>            run_instead_of_create(...);
std::optional<pair<entity_fields, OperationResult>>  run_instead_of_read(...);
std::optional<pair<vector<entity_fields>,
                   OperationResult>>                  run_instead_of_list(...);

// Triggers and jobs can call registered named queries:
nlohmann::json call_query(const std::string& query_name,
                          nlohmann::json& request);

Real trigger examples from the built-in plugins

TriggerPhase / TableWhat it demonstrates
HistoryCommonTrigger (core)After, "*", priority 1000Global audit: serializes every successful CRUDL operation to the history table as JSON; skips log tables and failed operations; runs last via high priority
BeforeCreateNoteTrigger (slip_box)Before, noteData preparation: computes sibling order and content linkage before persistence (uses FindNextSiblingOrderSQLiteQuery)
UpdateNotePathAndDepthAfterTrigger (slip_box)After, noteDerived-data maintenance: recomputes hierarchical path/depth for a whole subtree after a parent change
InsteadOfListDictionaryTermFulltextTrigger (dictionary)InsteadOf, virtual fulltext modelVirtual read: replaces the default list with a relevance-ranked fulltext search (exact > prefix > substring)
InsteadOfReadNoteNavigationTrigger (slip_box)InsteadOf, virtual note_navigationVirtual entity: assembles previous/next/parent/children/links for the graph exploration view — no table behind it
RReviewAfterCreateTrigger (repetition)After, r_reviewCross-model automation: each graded review updates the per-item scheduling state

Registering triggers

// In the plugin factory:
plugin->register_trigger(std::make_shared<triggers::RSessionBeforeCreateTrigger>());
plugin->register_trigger(std::make_shared<triggers::RReviewAfterCreateTrigger>());

// Trigger execution order within a phase is determined by priority:
// Lower integer = earlier execution
// Priority 1    → fires first
// Priority 1000 → fires last (e.g. HistoryCommonTrigger)

Job — Cron-Scheduled Background Tasks

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.

Job class (real signature)

#include <hive/api/Job.hpp>

class Job : public AbstractTriggerJob {
  Job(
      const std::string& job_name,        // unique — stored in job_entry
      const std::string& job_description,
      const std::string& cron_expression,
      bool enabled_by_default = true,
      bool run_once_when_missed = true    // catch up after downtime
  );

  // Called by a CronScheduler worker thread.
  // Return value: error string — empty string means success.
  virtual std::string run(cronq::JobConfig& job_config) = 0;

  // Jobs (like triggers) access data through registered named queries:
  nlohmann::json call_query(const std::string& query_name,
                            nlohmann::json& request);
};

JobConfig API (real)

// Per-job configuration stored with the job_entry record:
auto [val, err] = job_config.get_string("key");
auto days       = job_config.get_int_or_default("days", 30);
auto path       = job_config.get_string_or_default("export_path", "./export");
auto hash       = job_config.get_sha256();   // SHA-256 of the whole config

Real job examples

JobCronPattern it demonstrates
CleanupJob (core)@dailyRetention maintenance — deletes old logs/sessions/tokens via CleanupSQLiteQuery, thresholds from JobConfig
VacuumJob (core)@monthly, disabled by default, run_once_when_missed=falseHeavy maintenance that should never fire automatically on startup
HtmlExportJob (slip_box)configurableBatch export — renders the whole knowledge graph as navigable static HTML
DictionaryHtmlExportJob (dictionary)configurableDomain-specific export driven by JobConfig values

Cron expressions (Quartz-style via cronq)

"@daily"       // shorthand — once a day
"@monthly"     // shorthand — once a month
"0 2 * * 0"    // every Sunday at 02:00
"*/15 * * * *" // every 15 minutes
"0 9 * * 1"    // every Monday at 09:00

Job definitions are synchronized into the job_entry table at startup; every execution writes a job_run record with status and output. Missed schedules fire once at startup when run_once_when_missed=true.


Database Migrations — Naming and Integrity

Hive uses versioned migration scripts embedded as C++ raw string literals, with SHA-256 chain-hash integrity. The integrity system guarantees that applied migrations are never silently modified after deployment. Migration state is tracked in the schema_history table.

Naming convention (validated at startup)

V{sequence_number}__{descriptive_name}
Regex: V(\d+)__([a-zA-Z0-9_]+)

# Valid (real names from the built-in plugins):
V1__create_user
V2__create_dictionary_term
V3__create_dictionary_term_visit

# INVALID (will fail validation):
v1_create_user      # lowercase v
V1_create_user      # single underscore
V1__create user     # space in name

Registering migrations (real pattern)

Each plugin ships a *MigrationScripts class per database dialect that adds its scripts in order. Real excerpt from DictionarySQLiteMigrationScripts.cpp:

add_migration("V2__create_dictionary_term.sql", R"(
CREATE TABLE dictionary_term (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at DATETIME,
    updated_at DATETIME,

    dictionary_map_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    disambiguation TEXT,
    definition TEXT,
    status INTEGER NOT NULL,
    importance INTEGER NOT NULL,
    difficulty INTEGER NOT NULL,

    UNIQUE(dictionary_map_id, title, disambiguation),
    FOREIGN KEY(dictionary_map_id) REFERENCES dictionary_map(id)
);

CREATE INDEX idx_dictionary_term_map ON dictionary_term(dictionary_map_id);
CREATE INDEX idx_dictionary_term_title ON dictionary_term(title);
)");

// The plugin factory registers the scripts for the configured dialect:
REGISTER_MIGRATIONS(Dictionary, SQLite)
// The core plugin additionally ships PostgreSQL scripts:
// REGISTER_MIGRATIONS(Core, PostgreSQL)

Chain-hash integrity mechanism

// For each migration script in sequence order:
//   checksum   = SHA-256(sql_content)
//   chain_hash = SHA-256(previous_chain_hash || checksum)
//   Store: (name, checksum, chain_hash, applied_at) in schema_history
//
// On subsequent startups:
//   Recompute checksum of the embedded SQL
//   If checksum != stored: FATAL — migration was modified
//   Recompute chain_hash: if differs: FATAL — reordering detected
//
// Execution is transactional — a failed migration rolls back cleanly.
//
// Golden rule: NEVER edit a migration after it has been applied.
//              Add a new migration instead.

Complete Plugin — the Factory Pattern

Plugins are assembled by a factory that creates an api::Plugin instance and registers all contributions. This is the real pattern used by every built-in plugin (excerpt mirrors RepetitionPluginFactory.cpp):

MyPluginFactory.cpp

#include "hive/api/PluginFactory.hpp"
#include "hive/plugins/myplugin/models/MyModel.hpp"
#include "hive/plugins/myplugin/validators/MyModelValidator.hpp"
#include "hive/plugins/myplugin/triggers/MyAfterCreateTrigger.hpp"
#include "hive/plugins/myplugin/migrations/MyPluginSQLiteMigrationScripts.hpp"

namespace hive::plugins::myplugin
{
    api::PluginPtr MyPluginFactory::create(
        std::shared_ptr<api::RepositoryFactory>& repository_factory) const
    {
        auto plugin = std::make_shared<api::Plugin>(
            MY_PLUGIN_NAME,                        // "my_plugin"
            "My domain",                           // description
            std::vector<std::string>{"my_app"},   // frontend apps
            std::vector<std::string>{}            // explicit dependencies
        );  // an implicit dependency on "core" is always added

        // 1. Migrations — per configured database dialect
        REGISTER_MIGRATIONS(MyPlugin, SQLite)

        // 2. Models — ModelDefinition + validator + repository factory
        //    REGISTER_MODEL(model, Model, MODEL) expands to
        //    plugin->register_model(models::MODEL_DEFINITION,
        //        std::make_shared<validators::ModelValidator>(),
        //        repository_factory);
        REGISTER_MODEL(my_model, MyModel, MY_MODEL)

        // 3. Triggers, queries, jobs
        plugin->register_trigger(
            std::make_shared<triggers::MyAfterCreateTrigger>());
        plugin->register_query(
            std::make_shared<db::sqlite::queries::myplugin::FindMyThingsSQLiteQuery>());
        plugin->register_job(std::make_shared<jobs::MyExportJob>());

        return plugin;
        // close_for_changes() is called after registration —
        // the plugin becomes immutable at runtime
    }
}

CMakeLists.txt for the plugin (real pattern)

file(GLOB_RECURSE PLUGIN_MYPLUGIN_SOURCES
        hive/plugins/myplugin/**/*.cpp
        hive/plugins/myplugin/*.cpp
)

add_library(hive_plugin_myplugin STATIC ${PLUGIN_MYPLUGIN_SOURCES})

target_include_directories(hive_plugin_myplugin
        PUBLIC  ${CMAKE_SOURCE_DIR}/include/hive-plugin-myplugin
        PRIVATE ${CMAKE_SOURCE_DIR}/include/hive-essential
                ${CMAKE_SOURCE_DIR}/include/hive-model
                ${CMAKE_SOURCE_DIR}/include/hive-api
)

target_link_libraries(hive_plugin_myplugin
        PUBLIC hive_essential hive_model hive_api
)

Main.cpp factory registration (real pattern)

// Main.cpp defines:
#define REGISTER_PLUGIN(plugin, Plugin) \
    plugin_registry->register_plugin( \
        hive::plugins::plugin::Plugin##PluginFactory().create(repository_factory));

// register_plugins() picks the repository factory by database_type
// (SqliteRepositoryFactory today) and registers each enabled plugin:
REGISTER_PLUGIN(core,       Core)
REGISTER_PLUGIN(dictionary, Dictionary)   // behind MINDNET_ENABLE_DICTIONARY_PLUGIN
REGISTER_PLUGIN(myplugin,   MyPlugin)

Plugin Debugging Checklist

When a plugin isn't working as expected, work through this checklist in order:

1

Plugin loads at all?

Check server startup logs for the plugin registration output. If missing: verify the plugin is listed in allowed_plugins in hive.properties, the REGISTER_PLUGIN call exists in Main.cpp (and its CMake feature flag is ON), and the plugin name string matches exactly.

2

Migrations applied?

Check startup logs for migration output. If integrity error: revert the edited migration script or recreate the DB. If a migration is skipped: verify the V{n}__name naming — double underscore required.

sqlite3 hive.db \
  "SELECT * FROM schema_history ORDER BY id DESC LIMIT 10;"
3

Model appears in model_definition?

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.

4

CRUD endpoints accessible?

TOKEN="3f9c2a..."  # 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.

5

Triggers firing correctly?

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;"
6

Jobs running?

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.

7

Enable verbose logging

# configuration/hive.properties
max_log_level=DEBUG

Debug output shows request processing steps: auth context resolution, validator results, trigger execution, and scheduler activity. ModelCache::print_info() exposes cache hit/miss statistics.


Public API Header Quick-Reference

All types a plugin author needs are in include/hive-api/ and include/hive-model/.

HeaderKey types / constants
hive/api/Plugin.hppPluginregister_model(def, validator, repo_factory), register_trigger, register_query, register_job, register_migrations, register_library_file, close_for_changes, depends_on_plugins, get_apps
hive/api/PluginFactory.hppPluginFactory + REGISTER_MODEL / REGISTER_MIGRATIONS macros
hive/api/IValidator.hppIValidatorcan_create, can_read, can_update, can_delete, can_list (all take DbPtr& + AccessTokenContext&)
hive/api/IRepository.hppIRepositorycreate, read, update, remove, list, list_in_ids, list_ids
hive/api/Trigger.hppTrigger(name, description, priority, operations, phase, table)run_before_or_after, run_instead_of_create/read/update/delete/list, call_query
hive/api/TriggerPhase.hppTriggerPhase: Before=0 After=1 InsteadOf=2 Around=3
hive/api/Job.hpp + hive/api/cronq/JobConfig.hppJob(name, description, cron, enabled_by_default, run_once_when_missed)run(JobConfig&) → string; JobConfig::get_string / get_int_or_default / get_sha256
hive/api/OperationResult.hppOperationResult {status, error}, macros ok_result, status_403_forbidden, status_405_unsupported_operation
hive/api/AccessTokenContext.hppAccessTokenContext {user_id, msg, status, system}ok(), ko(), is_system()
hive/api/ModelCache.hppModelCache — LRU + TTL cache: get, put, invalidate, clear, shrink_to, set_ttl_ms, set_capacity_size
hive/model/ModelDefinition.hppModelDefinition(name, plugin_name) — fluent builder: set_group, set_columns, set_rest_operations, set_title_column, set_virtual_table, set_cache_enabled, add_custom_*_action
hive/model/ColumnDefinition.hppColumnDefinition(name, flags)set_default_value, set_description, set_enum_definition; all ColumnDefinitionFlag constants
hive/model/EnumDefinition.hppEnumDefinition — binds enum values to display strings for API/UI rendering
hive/essential/Crudl.hppCrudl enum: Create, Read, Update, Delete, List
hive/essential/AccessMode.hppAccessMode (0–8): MaintenanceMode … PublicFullAccess + access matrix helpers
hive/essential/UserRole.hppUserRole: Guest=0 Reader=1 Editor=2 Reviewer=3 Admin=4 SuperAdmin=5 System=100
hive/essential/DatabaseType.hppDatabaseType: Sqlite, PostgreSQL — multi-DB groundwork

Where Contributions Are Headed (from TODO.md)

The project TODO tracks several substantial engineering directions — useful context before proposing larger changes:

🗄

Database Abstraction & Multi-DB

Define IDatabase / IStatement / IMigration interfaces, refactor the SQLite layer behind them, then add PostgreSQL. The DatabaseType enum, db_* config keys, and CorePostgreSQLMigrationScripts already exist as groundwork.

📦

C++20/23 Modules Migration

Incrementally migrate from #include headers to C++ module units — faster builds, stronger encapsulation, cleaner dependency graph. Planned module-by-module, starting with leaf modules.

🧱

SQLite Constraint Emulation

Emulate NOT NULL / UNIQUE / FOREIGN KEY / DEFAULT with SQLite triggers where ALTER TABLE can't add them — without invalidating applied-migration checksums.

🔩

Rebuild Engine

A checklist-driven table-rebuild engine (create new table → copy → swap) for schema changes SQLite cannot express in place.

Ready to build?

Use the existing plugins (src/hive-plugin-slip-box/, src/hive-plugin-dictionary/) as authoritative structural templates — they demonstrate every extension point in production use.