Skip to content

Data model

Controller persistence is Postgres via Drizzle ORM, packaged as @rakkr/db (packages/db), which owns the schema, the committed migrations, and a migration verifier. Domain shapes are defined once in @rakkr/shared and imported by both the API and the console.

Postgres or fallback

DATABASE_URL is the master switch:

  • Set → each store uses its Postgres backend (PostgresNodeStore, PostgresRecordingJobStore, …). Several Postgres stores also hold a fallback and degrade to it on DB errors, so a transient database problem doesn’t take the whole controller down.
  • Unset → stores use a seeded in-memory or JSON-file fallback. The controller still serves many features; write operations that genuinely require a database throw a clear database_unavailable error. This is how most of the API test suite runs (the test harness removes DATABASE_URL unless RAKKR_API_TEST_DATABASE_URL is set).

Each fallback store has its own on-disk JSON path (e.g. RAKKR_RECORDING_METADATA_STORE_PATH); see the configuration reference.

The Drizzle client (packages/db/src/client.ts) opens a small postgres.js pool (max: 3) and re-exports the common query helpers used across the API.

Tables

The schema — assembled in packages/db/src/schema.ts from per-subsystem modules under packages/db/src/schema/ — defines 37 tables plus Postgres enums (node_status, health_severity, recording_status, recording_job_status, recording_chunk_status, recording_source, audit_outcome, access_policy_effect, access_policy_subject_type, room_roster_subject_type, room_roster_source). Timestamps are timestamptz; structured columns are jsonb.

Auth & access

TablePurpose
usersAccounts: email (unique), name, optional password hash (null for OIDC), provider, disabledAt.
roles, permissions, role_permissionsThe RBAC catalog and role→permission joins.
user_roles, access_groups, user_access_groupsRole and group membership; access_groups/user_access_groups back first-party group management, keyed by a name-derived immutable slug.
access_policiesAllow/deny rules by subject (user/group/everyone) and resource.
user_resource_grantsDirect per-user resource scope grants.
auth_sessionsLogin sessions keyed by token hash, with expiry/revocation and client context.
oidc_login_statesIn-flight OIDC PKCE/login state.

Nodes & audio

TablePurpose
nodesRecorder nodes: alias, hostname, agent version, status, last-seen, plus jsonb location/network/metadata/tags. roomId (FK → rooms, SET NULL) is the room-identity source of truth; the legacy jsonb location is retained for display.
node_credentialsNode enrollment tokens (stored as hashes) with prefix, last-used, revocation.
node_ssh_credentialsPer-node SSH keypairs for lifecycle: private key encrypted at rest, public half readable; the controller is the system of record.
node_bootstrap_tokensSingle-use, short-TTL day-0 bootstrap tokens (stored as hashes) consumed atomically at first contact.
audio_interfacesCapture devices per node: backend, channel count, hardware path, serial, sample rates.
audio_channelsChannels within an interface, with aliases.

Recordings, jobs & schedules

TablePurpose
recordingsRecording records: name, folder, source, status, health status, duration, checksum, node/schedule relations, jsonb metadata/tags, plus the cache paths — cachePath (default-playback file) and the optional rawCachePath/enhancedCachePath rendition masters.
recording_jobsCapture job lifecycle with the jsonb capture command and lease/heartbeat fields; indexed for lease-based claiming.
recording_chunksChunk lifecycle for a recording (recording_chunk_status: capturing/cached/uploading/uploaded/partial/failed).
schedulesRecurrence (jsonb), timezone, templates, capture overrides, and references to profile/retention/watchdog policies plus a uploadPolicyIds list for multi-destination fan-out (the singular uploadPolicyId is a retained-for-backfill legacy column). roomId (FK → rooms, RESTRICT) is the room-identity source of truth; the legacy room column is retained for display/templates.

Rooms & access rosters

TablePurpose
roomsFirst-class rooms: name, required site, optional building/floor/description/notes. Unique on (site, name); referenced by nodes.roomId and schedules.roomId.
room_rosterPer-room access grants: roomId (FK → rooms), subjectType (user/group), subjectId, jsonb capabilities, source (manual/calendar), sourceScheduleId, grantedByUserId.

Switchers

TablePurpose
switchersAudio-matrix switchers: host/port/model/mode/enabled, input/output counts, plus jsonb secrets (encrypted).
switcher_input_mapMaps a switcher input to a room (switcherId + inputroomId).
switcher_output_mapMaps a switcher output to a user (switcherId + outputuserId).

Health & audit

TablePurpose
health_eventsHealth/alert lifecycle: type, severity, status, optional node/recording/schedule, lifecycle timestamps + actors, jsonb details.
audit_eventsThe audit log: action, outcome, permission, actor + context, target, jsonb before/after/details/correlation IDs, reason.

Settings & uploads

TablePurpose
recording_profilesCodec/bitrate/channel-mode presets, plus the jsonb settings (including the voice-enhancement chain).
controller_settingsSingleton row for controller-wide settings (e.g. display name).
watchdog_policiesWatchdog rule sets (jsonb rules).
channel_map_templates, template_assignmentsChannel-map templates and their assignment to node/interface targets.
upload_destinationsNamed SMB/S3 upload targets — many per kind; each owns connection config and encrypted secrets.
upload_policiesPolicies selecting a destination + optional subfolder, trigger, retry budget, and delete-after-upload.
upload_queue_itemsThe retry queue — one item per policy per recording, indexed by due state.
upload_providersLegacy one-row-per-kind provider config, superseded by upload_destinations; retained for backfill.

Not tables: retention policies and node-lifecycle jobs are modeled in the fallback/seed layer (with a JSON store for lifecycle jobs) and audited via audit_events, rather than having dedicated tables.

Migrations

Migration SQL lives in packages/db/drizzle/*.sql with snapshots under drizzle/meta/; migrations are committed alongside schema changes (~47 to date, highest 0046). The workflow:

Terminal window
mise run db:generate # drizzle-kit generate — emit SQL from schema.ts
mise run db:migrate # drizzle-kit migrate — apply to DATABASE_URL
mise run db:verify # replay all migrations against an in-process PGlite database

Rules: edit schema.ts first, generate, review the emitted SQL + snapshot, run db:verify, and commit the generated files with the schema change. db:verify is part of the full mise run check gate and replays the migrations against an in-process PGlite (WASM Postgres) database, so it needs no Docker/Postgres server. Real-server migration application still runs in CI: the node:test-db concurrency harness applies the same migrations against a throwaway Postgres before its tests.

Shared contracts

@rakkr/shared is a set of Zod-based domain modules re-exported from packages/shared/src/index.ts, which both API and console import, keeping entity and request/response shapes in sync. It exports:

  • Domain schemas + inferred types for nearly every model (nodes, interfaces, meter frames, recordings, jobs, profiles, schedules, health/audit events, uploads, retention, channel maps, access control).
  • Enums mirroring the Postgres enums (status, severity, source, outcome, channel mode, …).
  • Recurrence schemasscheduleRecurrenceSchema is a discriminated union on mode (manual/once/daily/weekly/monthly/always_on) with start-early/stop-late and exceptions.
  • The RBAC source of truth — the Permission union (24 strings), the Role union (owner/admin/operator/viewer/auditor), the rolePermissions map, and hasPermission helpers. See the permissions reference.
  • Built-in defaults used by the fallback/seed layer (default voice profile, keep-controller-cache retention policy, scheduled-voice watchdog policy, and a test-only stub upload policy that is hidden from the console).

When a database is present, the permissions/roles/role_permissions tables persist the same catalog the shared package defines.