Hive exposes a consistent REST API auto-generated from ModelDefinition metadata. All endpoints return JSON. Authentication uses short-lived Bearer access tokens with rotating refresh tokens. Every model endpoint passes through a validator pipeline and trigger pipeline before touching the database.
http://<host>:<port>/api/v1/
Default: http://localhost:9000. The web frontend is served at /web/. HTTPS is recommended for production (configure a reverse proxy — nginx or Caddy — in front of Hive).
All protected endpoints require a Bearer access token:
Authorization: Bearer <access_token>
Access tokens are short-lived (default: 15 minutes, configurable as access_token_expires_in in minutes). Use the refresh token endpoint to obtain a new access token without re-authenticating. Refresh tokens rotate: each use issues a new refresh token (old one invalidated). Default refresh token lifetime: 30 days (refresh_token_expires_in=43200 minutes).
The frontend's apiFetch() function in frontend/api.js handles token lifecycle transparently:
// Proactive refresh: if access token expires in < 60 seconds, refresh first
// On 401 response: attempt token refresh once, then retry original request
// On refresh failure: redirect user to login screen
// Token storage: localStorage
// Keys: "access_token", "refresh_token", "token_expiry" (Unix timestamp)
| Token type | Default lifetime | Storage | Rotation |
|---|---|---|---|
| Access token | 15 minutes | localStorage (frontend) | Issued on login + every refresh call |
| Refresh token | 30 days | localStorage (frontend) | Rotates: each use invalidates old token and issues new one |
| Rotation threshold | 7 days before expiry | — | Controlled by refresh_token_rotation_threshold_in (minutes) |
All authentication endpoints live under /api/v1/auth/ and are handled by AuthEndpointsGenerator in hive-http. The implementation in frontend/auth.js wraps these endpoints.
| Method | Path | Description | Auth Required |
|---|---|---|---|
| POST | /api/v1/auth/login |
Authenticate with username + password. Returns access token, refresh token, and expiry. | No |
| POST | /api/v1/auth/logout |
Revoke the current session (invalidates the refresh token). Access token remains valid until natural expiry. | Access token |
| POST | /api/v1/auth/refresh_token |
Exchange a valid refresh token for a new access token + new refresh token. Old refresh token is invalidated. | Refresh token in body |
| POST | /api/v1/auth/register |
Register a new user account. Behaviour depends on registration_mode config: Free / RequiresAdminApproval / AdminAddsUsers. |
No (or Admin depending on mode) |
| POST | /api/v1/auth/change_password |
Change the authenticated user's password. Requires old password for verification. | Access token |
POST /api/v1/auth/login — Request:
Content-Type: application/json
{
"username": "admin",
"password": "your-secure-password"
}
Response 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsIn...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsIn...",
"expires_in": 900,
"token_type": "Bearer",
"user": {
"id": 1,
"username": "admin",
"role": 5
}
}
expires_in is in seconds. role matches UserRole enum: 0=Guest 1=Reader 2=Editor 3=Reviewer 4=Admin 5=SuperAdmin 100=System.
POST /api/v1/auth/refresh_token — Request:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsIn..."
}
Response 200 OK:
{
"access_token": "eyJhbGciOiJIUzI1NiIsIn...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsIn...",
"expires_in": 900
}
| Mode | Value | Behaviour |
|---|---|---|
Free | 0 | Anyone can register. New accounts receive default_user_role (default: Reader) and status Active. |
RequiresAdminApproval | 1 | Anyone can submit a registration request. Account gets status Pending. Admin must activate it. |
AdminAddsUsers | 2 | Self-registration is disabled. Only Admin or SuperAdmin can create user accounts via SuperAdmin API. |
// 401 Unauthorized — wrong credentials or expired token
{ "error": "Invalid credentials" }
// 403 Forbidden — authenticated but not permitted
{ "error": "Forbidden", "details": "Insufficient role" }
// 409 Conflict — username already taken
{ "error": "Username already exists" }
The single most important endpoint in Hive. The frontend reads this once at startup (cached in localStorage for 3 hours) and derives all navigation, CRUD forms, list columns, and FK dropdowns from it.
Handled by ModelDefinitionEndpointsGenerator. Returns the full ModelDefinition registry — one object per registered model across all loaded plugins.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/model_definition |
Optional (affects visible models) | Returns metadata for all registered models. Structure drives the entire frontend. |
[
{
"name": "note",
"plugin": "slip_box",
"group": "Slipbox",
"title_column": "title",
"operations": ["create", "read", "update", "delete", "list"],
"cache_enabled": true,
"readonly": false,
"columns": [
{
"name": "id",
"flags": 2056, // INTEGER | AUTO | HIDDEN bitmask
"type": "INTEGER",
"primary_key": true,
"hidden": true,
"auto": true,
"mandatory": false,
"mutable": false
},
{
"name": "title",
"flags": 257, // TEXT | MANDATORY bitmask
"type": "TEXT",
"hidden": false,
"mandatory": true,
"mutable": true,
"readonly": false
},
{
"name": "content",
"flags": 514, // TEXTAREA | MUTABLE
"type": "TEXTAREA",
"hidden": false,
"mandatory": false,
"mutable": true
},
{
"name": "parent_id",
"flags": 1028, // INTEGER | FOREIGN_KEY
"type": "INTEGER",
"foreign_key_model": "note",
"hidden": false,
"mandatory": false,
"mutable": true
}
],
"custom_list_actions": [],
"custom_create_actions": [],
"custom_read_actions": []
}
// ... more models
]
| Flag name | Bit value | Meaning in API response |
|---|---|---|
MANDATORY | 1 (1<<0) | Field is required on create |
UNIQUE | 2 (1<<1) | Database UNIQUE constraint |
FOREIGN_KEY | 4 (1<<2) | References another model — frontend renders as <select> |
AUTO | 8 (1<<3) | Auto-generated (PK, timestamp) — excluded from create/update body |
HIDDEN | 16 (1<<4) | Not shown in any frontend view |
READONLY | 32 (1<<5) | Shown in read view, never in edit form |
MUTABLE | 64 (1<<6) | Editable in update form |
INTERNAL | 128 (1<<7) | System-only field, excluded from API responses |
TEXT | 256 (1<<8) | Single-line text input |
TEXTAREA | 512 (1<<9) | Multi-line textarea (Markdown in Slipbox/Dictionary) |
INTEGER | 1024 (1<<10) | Integer input |
REAL | 2048 (1<<11) | Floating-point input |
BLOB | 4096 (1<<12) | Binary data |
BOOL | 8192 (1<<13) | Checkbox / boolean |
DATETIME | 16384 (1<<14) | Datetime input (ISO 8601 string in API) |
For every registered model with enabled operations, ModelEndpointGenerator generates these routes at startup. The set of enabled operations is controlled per model via ModelDefinition::set_rest_operations() or set_all_rest_operations().
| Method | Path | Operation (Crudl) | Request body | Description |
|---|---|---|---|---|
| GET | /api/v1/<model> |
List (5) | — | Paginated list of all records. Supports full QueryParams query string (see below). |
| GET | /api/v1/<model>/:id |
Read (2) | — | Fetch a single record by primary key. Runs can_read() validators. |
| POST | /api/v1/<model> |
Create (1) | JSON object (model fields) | Create a new record. Runs can_create() validators → Before triggers → repository.create() → After triggers. |
| PUT | /api/v1/<model>/:id |
Update (3) | JSON object (changed fields) | Update an existing record. Runs can_update() validators → Before triggers → repository.update() → After triggers. |
| DELETE | /api/v1/<model>/:id |
Delete (4) | — | Delete record by ID. Runs can_delete() validators → Before triggers → repository.remove() → After triggers (history written here). |
POST /api/v1/note — Request:
Content-Type: application/json
Authorization: Bearer <access_token>
{
"title": "My first note",
"content": "# Hello\n\nThis is **Markdown**.",
"parent_id": null,
"map_id": 3
}
Fields with AUTO flag (id, created_at, updated_at) must be omitted — they are set by the server.
Response 201 Created:
{
"id": 42,
"title": "My first note",
"content": "# Hello\n\nThis is **Markdown**.",
"parent_id": null,
"map_id": 3,
"path": "/maps/3/42",
"note_order": 0,
"created_at": "2026-06-26T10:30:00Z",
"updated_at": null
}
path is set by a Before trigger — not sent in the request body.
// GET /api/v1/note?page=1&page_size=20
{
"items": [ ... ], // array of entity objects
"total": 142, // total records matching query
"total_pages": 8, // ceil(total / page_size)
"page": 1,
"page_size": 20
}
// 400 Bad Request — missing mandatory field
{
"error": "Validation failed",
"details": "Field 'title' is mandatory and was not provided."
}
// 403 Forbidden — validator rejection
{
"error": "Forbidden",
"details": "You do not have permission to modify this record."
}
// 404 Not Found
{
"error": "Not found",
"details": "No record with id=999 in model 'note'."
}
// 405 Method Not Allowed — operation not enabled for this model
{
"error": "Method not allowed",
"details": "Operation 'delete' is not enabled for model 'history'."
}
The frontend's QueryParams class (in frontend/api.js) constructs the query string for list requests. All parameters are optional.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
page_size | integer | 20 | Records per page (max configurable, typically 100) |
sort | string | id | Column name to sort by |
order | asc | desc | asc | Sort direction |
q | string | — | Fulltext search query — if present, may trigger an InsteadOf search trigger (model-dependent) |
filter[field] | string | — | Equality filter on a column, e.g. filter[map_id]=3 |
ids | comma-separated integers | — | Fetch only specific IDs — used by list_in_ids() for FK label resolution |
// From frontend/api.js — QueryParams class
class QueryParams {
constructor() {
this.page = 1;
this.page_size = 20;
this.sort = 'id';
this.order = 'asc';
this.q = null; // search query
this.filters = {}; // { field: value }
}
toQueryString() {
// Serialises to URL query string
// Excludes null/undefined values
}
}
// list_all_entities() — auto-pages to collect up to 1000 records
// Used for FK dropdown population (SelectAll for referenced models)
async function list_all_entities(model_name, qp = new QueryParams()) {
let all = [];
let page = 1;
while (all.length < 1000) {
qp.page = page++;
const result = await list_entities(model_name, qp);
all = all.concat(result.items);
if (all.length >= result.total) break;
}
return all;
}
GET /api/v1/note?page=2&page_size=10&sort=created_at&order=desc&filter[map_id]=3
Authorization: Bearer <access_token>
Models can register named custom actions via ModelDefinition::add_custom_list_action(), add_custom_create_action(), and add_custom_read_action(). These appear as buttons in the frontend (implemented by executeCustomAction() in frontend/crud.js) and are dispatched to registered trigger handlers.
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/<model>/action/<action_name> |
List action | Fires for the entire model collection — used for bulk operations or model-level commands (e.g. export, sync) |
| POST | /api/v1/<model>/action/<action_name> |
Create action | Fires during the create form (before create) — used for preview, import, or pre-populate actions |
| POST | /api/v1/<model>/:id/action/<action_name> |
Read action | Fires for a specific entity — used for state transitions, send, duplicate, or export of individual records |
// In ModelDefinition builder (C++ plugin code)
ModelDefinition("note")
.add_custom_read_action("export_html", "Export as HTML")
.add_custom_read_action("duplicate", "Duplicate note")
.add_custom_list_action("export_all", "Export all notes")
.add_custom_create_action("import_md", "Import from Markdown file")
Operational endpoints handled by InfoHealthEndpointsGenerator and SuperAdminEndpointsGenerator.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | Liveness probe. Returns 200 OK when the server is running and the SQLite database is reachable. |
| GET | /info |
No | Build and runtime metadata: version string, loaded plugins, startup timestamp, non-sensitive config values. |
| GET | /api/v1/super_admin/users |
SuperAdmin | List all users including pending and deactivated. SuperAdmin-only. |
| PUT | /api/v1/super_admin/users/:id |
SuperAdmin | Modify user role or status (activate, deactivate, ban). |
| GET | /api/v1/super_admin/jobs |
SuperAdmin | List all scheduled jobs with their current status and next run time. |
| PUT | /api/v1/super_admin/jobs/:id |
SuperAdmin | Enable or disable a job, update cron expression. |
GET /health — Response:
HTTP/1.1 200 OK
{
"status": "ok",
"db": "ok"
}
GET /info — Response:
{
"version": "2.0.0",
"plugins": ["core", "slip_box",
"dictionary", "repetition"],
"started_at": "2026-06-26T08:00:00Z",
"access_mode": "PublicFullAccess",
"registration_mode": "Free"
}
Static files are served by WebEndpointsGenerator from the directory specified by --frontend-path / -s flag (default: ./frontend).
| Path | Description |
|---|---|
/web/ | Hive web frontend landing page |
/web/index.html | Main application — generic CRUD for all models from all plugins |
/web/app_slip_box.html | Slipbox-specific application — graph exploration view, note editor, tree navigation |
/web/app_dictionary.html | Dictionary application — term search, detail view with aliases and understanding levels |
/web/app_repetition.html | Repetition review session application — card-by-card review with rating buttons |
/web/js/api.js | Frontend API client module (ES module, loaded directly) |
/web/js/crud.js | Frontend CRUD rendering module |
Complex JSON-based filter expressions are a planned enhancement. The design is documented in the project backlog. Currently only simple equality filters (filter[field]=value) and search query (q) are available.
| Operator | JSON format | Description |
|---|---|---|
AND | {"and": [expr_A, expr_B]} | Logical conjunction — all conditions must match |
OR | {"or": [expr_A, expr_B]} | Logical disjunction — at least one condition matches |
NOT | {"not": expr} | Logical negation |
eq | {"field": {"eq": value}} | Equality check |
neq | {"field": {"neq": value}} | Inequality check |
lt / gt | {"field": {"lt": value}} | Less than / greater than |
lte / gte | {"field": {"lte": value}} | Less-or-equal / greater-or-equal |
in | {"field": {"in": [a, b, c]}} | Value in list |
like | {"field": {"like": "%text%"}} | SQL LIKE pattern match |
is_null | {"field": {"is_null": true}} | Null check |
// GET /api/v1/note?filter={"and":[{"map_id":{"eq":3}},{"title":{"like":"%hive%"}}]}
{
"and": [
{ "map_id": { "eq": 3 } },
{ "title": { "like": "%hive%" } },
{ "or": [
{ "parent_id": { "is_null": true } },
{ "note_order": { "gt": 0 } }
]}
]
}