Skip to main content
Version: Next

Data model

The tables below are taken from the server migrations (server/internal/storage/sql/migrations/) and the client mirror schema (app/src/lib/local/schema.ts). Column names are identical on both sides and are the keys used in sync payloads. Enum columns store the proto number; the display label is what the app shows (app/src/lib/i18n/locales/en.json).

Every synced table carries three bookkeeping columns not repeated below: organization_id (tenant), field_hlc (JSON field clock, see the sync protocol) and deleted (soft-delete flag). Ids are UUIDs, minted on the device or by the server for rows created through the API. Timestamps are stored as ISO 8601 strings on the client and as TIMESTAMP on the server.

Hierarchy​

apiary
└── hive ── queen (one active, older ones kept with replaced_at set)
├── inspection
├── task (task.hive_id and task.apiary_id are both optional)
├── harvest
├── treatment
├── placement (which apiary the hive lived in, and when)
└── event (append-only history with frozen apiary/hive/queen)

Sharing and sync partitioning use scope_id: an apiary's own id for the apiary and everything under it. Only event stores scope_id as a column; for the other tables it is carried on the Change message.

apiary​

ColumnTypeNotes
idtext
nametextrequired
addresstextfree text
lat, lngreal0 when unset
notetext
created_at, updated_attimestamp

hive​

ColumnTypeNotes
idtextalso encoded in the QR label
apiary_idtextcurrent apiary
nametext
typeintHiveType, see below
statusintHiveStatus, see below; new hives start at 1
boxesintnumber of boxes
colony_origintexte.g. "swarm 2024"
notetext
qr_codetextreserved; the printed code is derived from id by shortCode() in app/src/lib/qr.ts
phototextdata URL or blob key
created_at, updated_attimestamp

HiveType: 0 Unspecified, 1 Zander, 2 Dadant, 3 Deutsch Normal, 4 Langstroth, 5 Warré, 6 Top-bar, 99 Other.

HiveStatus: 0 Unspecified, 1 Active, 2 Nucleus, 3 Queenless, 4 Lost, 5 Dissolved.

queen​

ColumnTypeNotes
idtext
hive_idtext
yearintthe queen's year; drives the default marking colour
markingintMarkingColor: 1 white (years ending 1/6), 2 yellow (2/7), 3 red (3/8), 4 green (4/9), 5 blue (5/0); defaults from year
origintext
breeder_numbertext
introduced_attimestampstart of reign
replaced_attimestampend of reign, null while reigning
activebooltrue for the current queen
notetext
created_at, updated_attimestamp

A queen change sets active = 0 and replaced_at on the old row and inserts a new one; old queens are never deleted.

inspection​

ColumnTypeNotes
id, hive_idtext
datetimestamp
weathertext
queen_seen, eggs_seenbool
temperamentint1 Very gentle, 2 Gentle, 3 Normal, 4 Nervous, 5 Aggressive
calmnessint1 Runs off comb, 2 Restless, 3 Calm, 4 Very calm
framesintoccupied frames
brood_framesint
storesint1 Good, 2 Medium, 3 Low, 4 None
queen_cellsintswarm cells counted
youngest_larvaintage in days of the youngest larva seen
covered_larvaboolcapped brood seen
varroatext
honey_kg, fed_kg, weight_kgreal
frames_added, frames_removedint
drone_frame_cut, super_addedbool
temp_hive, temp_outsidereal°C, null when not measured in the app; CreateInspection stores 0 for fields left out
humidity_hive, humidity_outsidereal%, same rule as the temperatures
notetext
photo_keystextOR-Set JSON, the only set column
created_attimestamp

task​

ColumnTypeNotes
idtext
titletext
hive_id, apiary_idtextoptional
due_attimestamp
donebool
priorityintTaskPriority: 1 Low, 2 Normal, 3 High; the server column defaults to 2, the app writes 0
note, recurrence, assigned_totextpresent in the schema; the task form only fills title and due_at
created_attimestamp

placement​

ColumnTypeNotes
id, hive_id, apiary_idtext
start_attimestamp
end_attimestampnull for the current placement

Creating a hive opens a placement; moving it closes the open row at the move time and opens a new one.

A task without apiary_id syncs under the personal scope user:<user id> instead of an apiary id.

harvest​

ColumnTypeNotes
idtext
apiary_id, hive_id, queen_idtextfrozen at harvest time
datetimestamp
varietytext
amount_kgreal
water_contentreal%
batch_numbertext
best_beforetimestamp
notetext

treatment​

ColumnTypeNotes
idtext
apiary_id, hive_id, queen_idtextfrozen at treatment time
datetimestamp
product, active_ingredienttext
dose, methodtext
batch_numbertext
withdrawal_untiltimestamp
reasontextdefaults to varroa
notetext

event​

ColumnTypeNotes
idtext
scope_idtextapiary id
typeintEventType, see below
datetimestamp
apiary_id, hive_id, queen_idtextfrozen at event time
ref_entity, ref_idtextthe detail row, e.g. harvest / its id
titletext
amount_kgrealcopied from the harvest so yield queries need no join
detailtextJSON
author_idtext

EventType: 1 Created, 2 Queen introduced, 3 Queen replaced, 4 Moved, 5 Inspection, 6 Treatment, 7 Harvest, 8 Status, 9 Dissolved. The app writes types 1 to 7 (app/src/lib/local/history.ts). See History and events.

Server-only tables​

organization, users, member, invite, user_passkey, api_key, apiary_share, change_log and seq_counter exist only on the server. member.role is owner or member; users.role is admin or user. apiary_share is read by the sync scope check but nothing in the app writes it. The client adds outbox and sync_meta for the sync engine.

api_key​

Long-lived keys for scripts and integrations (migration 0014_api_keys.sql, server/internal/auth/apikey.go). Not synced.

ColumnTypeNotes
idtext
user_idvarchar(64)owner; indexed
organization_idvarchar(64)the tenant the key acts in, fixed at creation
nametextlabel shown in Settings
key_prefixtextfirst 12 characters of the plaintext, for display
key_hashvarchar(64)hex SHA-256 of the plaintext obhk_... token, unique; the plaintext is never stored
scopetextwrite (default) or read; a read key may call only Get*, List*, Pull and Subscribe
created_attimestamp
expires_attimestampnull for keys that never expire; a key past this time is refused
last_used_attimestampupdated on every verified use, null until then

A key is verified by hashing the bearer token and looking up key_hash; the row is deleted on removal, and a key whose owner is no longer a member of organization_id, or whose expires_at has passed, is refused without being deleted.