Skip to main content

Database

Open Genie uses PostgreSQL with Drizzle ORM for type-safe database access and migrations.

Setup

The database client is initialized in lib/db/index.ts using the postgres driver:

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });

The Drizzle config (drizzle.config.ts) points to lib/db/schema.ts for schema definitions and outputs migrations to ./drizzle/.

Schema

All tables are defined in lib/db/schema.ts. Below is a reference for every table.

Users & Devices

users

ColumnTypeDescription
iduuid (PK)Auto-generated
nametextDisplay name
emailtext (unique)Email address
createdAttimestampAuto-set

devices

ColumnTypeDescription
iduuid (PK)Auto-generated
userIduuid (FK → users)Owner
nametextDevice display name
typetext"phone" | "tablet" | "tv" | "web"
pushTokentextExpo/Firebase push token
lastSeentimestampLast WebSocket disconnect time
createdAttimestampAuto-set

pairingCodes

ColumnTypeDescription
iduuid (PK)Auto-generated
codetext (unique)6-character pairing code
userIduuid (FK → users)Who generated the code
expiresAttimestampCode expiration (typically 10 min)
usedAttimestampWhen code was consumed
deviceIduuidDevice that used the code

Conversations & Messages

conversations

ColumnTypeDescription
iduuid (PK)Auto-generated
deviceIduuid (FK → devices)Owning device
titletextConversation title (auto-generated or manual)
createdAttimestampAuto-set
updatedAttimestampAuto-updated

messages

ColumnTypeDescription
iduuid (PK)Auto-generated
conversationIduuid (FK → conversations)Parent conversation
roletext"user" | "assistant" | "system" | "tool"
contenttextMessage text
toolCallsjsonbTool call metadata (if role = assistant)
toolResultjsonbTool execution result (if role = tool)
createdAttimestampAuto-set

Memory

memoryEntries

ColumnTypeDescription
iduuid (PK)Auto-generated
categorytextGrouping (e.g., "family", "preferences")
keytextEntry identifier within category
valuetextStored information
sourcetextHow it was created ("user" | "extraction" | "api")
createdAttimestampAuto-set
updatedAttimestampAuto-updated

Unique constraint: (category, key) — upserts update existing entries.

Media

mediaFiles

ColumnTypeDescription
iduuid (PK)Auto-generated
pathtext (unique)Relative path within storage
nametextFilename
typetext"video" | "audio" | "image" | "document"
mimeTypetextMIME type
sizebigintFile size in bytes
durationrealDuration in seconds (video/audio)
widthintegerWidth in pixels (video/image)
heightintegerHeight in pixels (video/image)
thumbnailPathtextPath to generated thumbnail
metadatajsonbExtended metadata (EXIF, music tags, etc.)
lastPositionrealLast playback position in seconds
createdAttimestampAuto-set
updatedAttimestampAuto-updated

playlists

ColumnTypeDescription
iduuid (PK)Auto-generated
nametextPlaylist name
descriptiontextOptional description
createdAttimestampAuto-set

playlistItems

ColumnTypeDescription
iduuid (PK)Auto-generated
playlistIduuid (FK → playlists)Parent playlist
mediaFileIduuid (FK → mediaFiles)Media file
positionintegerSort order within playlist
addedAttimestampAuto-set

Automation

cronJobs

ColumnTypeDescription
iduuid (PK)Auto-generated
nametextJob display name
scheduletextCron expression (e.g., "0 * * * *")
actionTypetextAction to execute
actionPayloadjsonbParameters for the action
enabledbooleanWhether the job is active
lastRunAttimestampLast execution time
nextRunAttimestampNext scheduled execution
createdAttimestampAuto-set

jobRuns

ColumnTypeDescription
iduuid (PK)Auto-generated
jobIduuid (FK → cronJobs)Parent job
statustext"running" | "success" | "error"
outputjsonbExecution result
errortextError message (if failed)
startedAttimestampAuto-set
finishedAttimestampCompletion time

webhooks

ColumnTypeDescription
iduuid (PK)Auto-generated
nametextWebhook display name
secrettextShared secret for validation
actionTypetextAction to execute on trigger
actionPayloadjsonbDefault parameters (merged with webhook data)
enabledbooleanWhether the webhook is active
lastTriggeredAttimestampLast trigger time
createdAttimestampAuto-set

IoT Devices

Hardware integrations — ESP32 music boxes, Tuya smart plugs, Bluetooth audio sinks — are tracked separately from paired client devices.

iotDevices

ColumnTypeDescription
iduuid (PK)Auto-generated
namevarchar(255)Display name (e.g. "Living Room Music Box")
kindvarchar(32)"esp32_music_box" | "tuya_plug" | "bluetooth_audio"
externalIdvarchar(255)Hardware ID — ESP32 MAC, Tuya deviceId, or BT MAC
configjsonbPer-kind config (local key, auto-connect flag, …). Secrets stay in env/settings.
lastSeentimestampLast WS connect or API ping
createdAttimestampAuto-set

Indexes on kind and externalId for fast lookups by the WS handler.

iotEvents

Append-only event log — every state change is a row. Rows older than 30 days are pruned automatically.

ColumnTypeDescription
iduuid (PK)Auto-generated
iotDeviceIduuid (FK → iotDevices, SET NULL on delete)Source device
eventvarchar(64)Event name: "connected", "play", "stop", "bt_connect", …
detailtextHuman-readable description
levelvarchar(16)"info" | "warn" | "error"
createdAttimestampAuto-set

Indexes on createdAt (for time-range queries) and iotDeviceId (for per-device views).

Cameras

cameraConfigs

ColumnTypeDescription
iduuid (PK)Auto-generated
nametextCamera display name
urltextRTSP or HTTP stream URL
typetext"rtsp" | "http" | "mjpeg"
captureIntervalSecintegerSeconds between captures (default: 60)
visionPrompttextCustom prompt for vision analysis
alertRulesjsonbArray of alert rule definitions
enabledbooleanWhether capture is active
createdAttimestampAuto-set

cameraEvents

ColumnTypeDescription
iduuid (PK)Auto-generated
cameraIduuid (FK → cameraConfigs)Source camera
imagePathtextPath to captured image
analysisjsonbParsed vision response
alertLeveltextnull | "info" | "warning" | "critical"
alertMessagetextAlert description (if triggered)
createdAttimestampAuto-set

Notifications & Reminders

notifications

ColumnTypeDescription
iduuid (PK)Auto-generated
deviceIduuid (FK → devices)Target device
titletextNotification title
bodytextNotification body
leveltext"info" | "warning" | "critical"
categorytextPush notification category
datajsonbExtra payload data
statustext"pending" | "sent" | "delivered" | "read"
createdAttimestampAuto-set
readAttimestampWhen user read it

reminders

ColumnTypeDescription
iduuid (PK)Auto-generated
titletextReminder text
dueAttimestampWhen to trigger
repeattextRepeat schedule (cron or keyword)
completedbooleanWhether it's done
createdAttimestampAuto-set

Plugins

Three tables manage the plugin runtime. Non-secret preferences live in data/plugins/<slug>/settings.json (on-disk, human-editable). Sensitive values are stored only in the database.

plugins

Install registry — source of truth for whether a plugin is installed and enabled.

ColumnTypeDescription
iduuid (PK)Auto-generated
slugvarchar(100) (unique)Plugin identifier — matches the folder name
versionvarchar(32)Installed semver version
enabledbooleanWhether the plugin is running
sourcevarchar(16)"upload" | "hub"
permissionsGrantedjsonbPermissions the user approved at install time
manifestjsonbFull parsed manifest.json
installedAttimestampAuto-set
enabledAttimestampSet when the plugin is first enabled

pluginOauthTokens

One row per (plugin, provider). Tokens are encrypted at rest.

ColumnTypeDescription
iduuid (PK)Auto-generated
pluginSlugvarchar(100)Plugin that owns this token
providervarchar(32)"google" | "slack" | "github" | "microsoft"
accessTokenEnctextAES-256-GCM encrypted access token
refreshTokenEnctextAES-256-GCM encrypted refresh token
expiresAttimestampToken expiry
scopesjsonbGranted OAuth scopes
accountLabelvarchar(255)Display string shown in the UI (e.g. "you@gmail.com")
updatedAttimestampAuto-updated

Index on pluginSlug for fast per-plugin lookups.

pluginSecrets

Encrypted key-value store for secret-type settings fields and anything the plugin stores via genie.secrets.*.

ColumnTypeDescription
pluginSlugvarchar(100)Owning plugin
keyvarchar(100)Secret name
encryptedValuetextAES-256-GCM encrypted value
updatedAttimestampAuto-updated

Compound index on (pluginSlug, key) (acts as a composite primary key).

Encryption key: GENIE_SECRETS_KEY env var (32-byte hex). If unset, plugins that declare secret fields or OAuth providers cannot be enabled.

Relations

Key foreign-key relationships:

users ──< devices ──< conversations ──< messages
──< notifications

playlists ──< playlistItems >── mediaFiles

cameraConfigs ──< cameraEvents

cronJobs ──< jobRuns

iotDevices ──< iotEvents

plugins (no FK — linked to on-disk folder by slug)
pluginOauthTokens (linked to plugins by pluginSlug)
pluginSecrets (linked to plugins by pluginSlug)

All cascading deletes are configured so removing a parent removes its children.

Migrations

# Generate a migration from schema changes
npm run db:generate

# Apply pending migrations
npm run db:migrate

# Push schema directly (development only, may lose data)
npm run db:push

# Open Drizzle Studio for visual exploration
npm run db:studio

Migration files are stored in drizzle/ and should be committed to version control.