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", "access_token_expires_at"
| 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 (real keys):
{
"user_id": 1,
"access_token": "3f9c2a…",
"access_token_expires_at": 1782115200,
"refresh_token": "b81d44…",
"refresh_token_expires_at": 1784707200
}
Expiry values are Unix timestamps. The user's role (0=Guest 1=Reader 2=Editor 3=Reviewer 4=Admin 5=SuperAdmin 100=System) is resolved server-side from the user table on each request. Tokens are random values stored server-side only as SHA-256 hashes.
POST /api/v1/auth/refresh_token — Request:
{
"refresh_token": "b81d44…"
}
Response 200 OK:
{
"user_id": 1,
"access_token": "a4e7f1…",
"access_token_expires_at": 1782116100,
"refresh_token": "c92e55…" // present only when
// the token was rotated
}
The refresh token rotates only when its remaining lifetime falls below refresh_token_rotation_threshold_in (default 7 days) — not on every call.
| 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",
"map_id": 3,
"parent_note_id": null
}
Fields with the AUTO flag (id, created_at, updated_at) must be omitted — they are set by the server. Hierarchy fields (sibling_order, path, depth) are computed by the note triggers.
Response 200 OK:
{
"id": 42,
"title": "My first note",
"map_id": 3,
"parent_note_id": null
}
Create echoes the request body back with the new record's "id" added. Fetch the full record — including trigger-computed fields — with GET /api/v1/note/42.
// GET /api/v1/note?page_number=1&page_size=20
{
"items": [ ... ], // array of entity objects
"total_items": 142, // total records matching query
"total_pages": 8, // ceil(total_items / page_size)
"page_number": 1,
"page_size": 20
}
Errors are returned as plain-text messages with the HTTP status coming from the validator's OperationResult or the endpoint checks:
// 400 Bad Request — invalid paging
Invalid page size. It must be 5 at least
// 401 Unauthorized — missing/expired/revoked token
(from AccessTokenContext.status)
// 403 Forbidden — validator rejection
You are not authorized...
// 405 Method Not Allowed — operation not enabled for this model
Method not allowed for model history.
// 500 Internal Server Error — persistence failure
Saving the note failed. Error: ...
List endpoints accept these query-string parameters, parsed by ModelEndpointGenerator. All parameters are optional.
| Parameter | Type | Default | Description |
|---|---|---|---|
page_number | integer | 1 | Page number (1-indexed, must be positive — else 400) |
page_size | integer | 20 | Records per page — validated range 5–100 (else 400) |
sort | string | — | Column name to sort by |
order | asc | desc | asc | Sort direction |
fields | comma-separated names | all visible | Field selection — return only the listed columns |
<column_name>=<value> | string | — | Direct field filter: any column of the model can be used as a query parameter, e.g. ?map_id=3 (the id column is excluded) |
// From frontend/api.js — QueryParams is a small builder over a Map
export class QueryParams {
#params = new Map();
add(key, value) { ... } // add any parameter
sort(sort, order = null) { ... } // convenience for sort + order
toString() { ... } // serialise to query string
}
// list_entities(entity, additional_params, page_number = 1, page_size = 20)
// sets page_number + page_size and appends additional params.
// list_all_entities() — auto-pages with page_size=100 until all
// records are collected (used for FK dropdown population).
GET /api/v1/note?page_number=2&page_size=10&sort=created_at&order=desc&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(). Custom actions are metadata, not extra REST routes: they ship in the /api/v1/model_definition response and the frontend renders them as navigation buttons that jump to another model's list/create/read view with pre-filled parameters.
// C++ side — in ModelDefinition builder (plugin code):
// add_custom_action(Crudl operation, target model, label, params map)
ModelDefinition md("note", "slip_box");
md.add_custom_read_action("note_navigation", "Explore",
{{"note_id", "{id}"}})
.add_custom_list_action("wanted_note", "Wanted notes", {});
// Serialized into model_definition → consumed by crud.js:
{
"crudl": "Read", // which view to open
"model_name": "note_navigation", // target model
"label": "Explore", // button text
"params": { "note_id": "{id}" } // "{id}" = current record id
}
The frontend groups action buttons by target model on the read view. The "{id}" placeholder is substituted with the current entity's ID, which makes custom actions the bridge between generated CRUD screens and virtual models like note_navigation (backed by InsteadOf triggers).
Operational endpoints handled by InfoHealthEndpointsGenerator and SuperAdminEndpointsGenerator.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | Liveness probe. Returns 200 OK with server uptime while the server is running (disabled in MaintenanceMode). |
| GET | /info |
No | Build and runtime metadata: name, description, version, build time, environment, host/ports, access mode, registration mode, default user role. |
| GET | /api/v1/superadmin/configure |
SuperAdmin | Built-in HTML configuration UI — edit all runtime configuration keys; fields that need a restart are marked, with an optional scheduled restart. |
| POST | /api/v1/superadmin/configure |
SuperAdmin | Save configuration changes. Values are validated per key (e.g. token lifetime bounds); errors are returned per field. |
| POST | /api/v1/superadmin/restart |
SuperAdmin | Restart the server process. Audited to super_admin_log. |
| POST | /api/v1/superadmin/shutdown |
SuperAdmin | Stop the server process. Audited to super_admin_log. |
Requests from users below UserRole::SuperAdmin receive 403 Forbidden: only SuperAdmin can perform this action.
GET /info — Response (real keys):
{
"name": "Hive",
"description": "…",
"version": "…",
"build_time": "…",
"environment": "Production",
"host": "http://localhost",
"port": 9000,
"frontend_port": 9000,
"access_mode": "PublicFullAccess",
"registration_mode": "Free",
"default_user_role": "Reader"
}
GET /health — Response:
HTTP/1.1 200 OK
Server is healthy — includes formatted
uptime (seconds/minutes/hours/days
since startup).
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 — lists the apps of all loaded plugins |
/web/index.html | Main application — generic CRUD for all models from all plugins |
/web/app_slip_box.html | Slipbox application — graph exploration view, note editor, tree navigation |
/web/app_simple_slip_box.html | Simplified Slipbox reading interface (previous/next navigation) |
/web/app_dictionary.html | Dictionary application — 19-language UI, term search with autocomplete, metrics windows |
/web/app_repetition.html | Repetition review session application — card-by-card review with grading |
/web/api.js, /web/crud.js, … | Frontend ES modules, served directly from the frontend root (no bundler) |
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 } }
]}
]
}