Skip to main content

Schema

Define your extension's database tables in server/schema.ts using defineSchema and defineTable from @zaflun/lumio-sdk/server.

Basic example

// server/schema.ts
import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";

export default defineSchema({
rules: defineTable({
text: v.string(),
revealed: v.boolean(),
score: v.number().optional(),
tags: v.array(v.string()).optional(),
}).storage("instance"),

votes: defineTable({
choice: v.string(),
userId: v.string(),
weight: v.number().optional(),
}).storage("account"),
});

defineSchema

Wraps a map of table definitions. Each key is the table name used in functions (queryRows("rules"), etc.).

import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";

export default defineSchema({
tableName: defineTable({ /* field validators */ }).storage("scope"),
// ...
});

defineTable

Defines the columns of a table. Accepts a record of field names to validators.

defineTable({
fieldName: validator,
// ...
})

Every table automatically gets these system columns — you do not define them:

  • idtext primary key (UUID, auto-generated)
  • created_attimestamptz (auto-set on insert)
  • updated_attimestamptz (auto-updated on patch)

Validators

ValidatorTypeExample
v.string()TEXTv.string()
v.number()DOUBLE PRECISIONv.number()
v.boolean()BOOLEANv.boolean()
v.array(v.string())TEXT[]v.array(v.string())
v.array(v.number())DOUBLE PRECISION[]v.array(v.number())
v.object({ ... })JSONBv.object({ x: v.number(), y: v.number() })

.optional() modifier

Appending .optional() makes the field nullable in SQL and optional in TypeScript:

defineTable({
text: v.string(), // required, NOT NULL
score: v.number().optional(), // nullable, REAL | null
notes: v.string().optional(), // nullable, TEXT | null
})

Storage scopes

Chain .storage(scope) to control which installation instances share the same table data:

ScopeDescriptionUse case
"instance"One set of rows per overlay installRules for a specific overlay
"overlay"Shared across all installs on the same overlayPer-overlay leaderboard
"account"Shared across all overlays in an accountAccount-wide vote history
"global"Shared across all installs of this extension everywhereGlobal leaderboard
export default defineSchema({
rules: defineTable({
text: v.string(),
revealed: v.boolean(),
}).storage("instance"), // each overlay install has its own rules

globalLeaderboard: defineTable({
userName: v.string(),
score: v.number(),
}).storage("global"), // all installs share this leaderboard
});

Migrations

Schema changes are additive only — you can add new tables and add nullable columns to existing tables. Removing tables or columns, or changing column types, requires a manual migration step via the extension dashboard.

warning

Never rename a field in defineTable without a migration plan. The old column name remains in PostgreSQL; the new name creates a new column.

Full example

// server/schema.ts
import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";

export default defineSchema({
// Challenge rules — one set per overlay install
rules: defineTable({
text: v.string(),
revealed: v.boolean(),
category: v.string().optional(),
}).storage("instance"),

// Votes — shared across the account
votes: defineTable({
ruleId: v.string(),
userId: v.string(),
choice: v.string(),
}).storage("account"),

// Config snapshots — per instance
snapshots: defineTable({
label: v.string(),
data: v.object({
homeTeam: v.string(),
awayTeam: v.string(),
homeScore: v.number(),
awayScore: v.number(),
}),
}).storage("instance"),
});