Skip to main content

Schema

Define your extension's database tables in server/schema.ts using defineSchema and defineTable from @zaflun/lumio-sdk/server. The tables back the declarative server functions (queryRows, insertRow, patchRow, deleteRow) and the bot-module ctx.db API.

Basic example​

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

export default defineSchema({
points: defineTable({
user: v.string(),
score: v.number(),
badges: v.array(v.string()).optional(),
}),

rules: defineTable({
text: v.string(),
revealed: v.boolean(),
meta: v.object({ category: v.string() }).optional(),
}),
});

Each key of defineSchema is a table name, used in functions (queryRows("points"), ctx.db.list("points")).

Column types​

ValidatorPostgreSQL typeJSON value
v.string()TEXTstring
v.number()DOUBLE PRECISIONnumber (a JS number, so 1.5 works)
v.boolean()BOOLEANboolean
v.array(...)JSONBarray
v.object({ ... })JSONBobject

Every value is checked against its column type before it reaches the database. A string for a v.number() column, or null for a required column, is rejected with invalid value for column ... / column ... is not nullable.

.optional()​

.optional() makes the column nullable. Without it the column is NOT NULL, and every insert must set it.

defineTable({
text: v.string(), // required, NOT NULL
score: v.number().optional(), // nullable
})

System columns​

Every table gets four columns you do not declare. Every row you read carries them:

ColumnTypeSet by
_idUUIDgenerated on insert; use it for get / patch / delete
_account_idUUIDthe account of the install that wrote the row (never from your code)
_created_attimestampinsert
_updated_attimestampinsert and every patch

Names starting with _ are reserved: you cannot declare, write or filter on them.

Names and limits​

WhatLimit
Table and column nameslowercase letter first, then a-z, 0-9, _; at most 40 characters; not pg_*; tables not ext_*
Tables per extension32
Columns per table64
Schema size64 KiB

Storage scope​

instance is the only supported scope and the default: the rows of a table belong to the account of the install that wrote them. .storage("overlay"), .storage("account") and .storage("global") are rejected at upload. See Storage scoping.

Uploading the schema​

The schema is part of a version. Send the defineSchema result (JSON.stringify of the object) as data_schema when you create the version:

  • REST: the data_schema field of POST /v1/developer/extensions/{id}/versions (JSON) or of the multipart POST /v1/developer/extensions/{id}/versions/upload.
  • GraphQL: dataSchema on createExtensionVersion / uploadExtensionVersion.

lumio deploy does this for you: it bundles server/schema.ts (or server/schema.js / server/schema.mjs), takes its default export and sends it as data_schema with the version. An extension without a schema file uploads no data_schema. A version without data_schema declares no data: its ctx.db and server-function calls answer this extension has no data schema.

Lumio validates the schema at upload. An invalid schema (a bad name, an unsupported type or scope, too many tables) answers 422 with code INVALID_DATA_SCHEMA.

When the tables are created​

Lumio creates the tables when a reviewer approves the version (into testing or published). The first install of an approved version creates them too, for example after the last uninstall removed them. Draft versions get no tables; use lumio dev to develop against a local mock.

All installs of your extension share the same tables. Each row is scoped to its account, so an install only ever sees its own account's rows.

Changing the schema​

A new version may only add to the schema. Older versions that accounts have pinned keep running on the same tables, so nothing they rely on may change:

ChangeAllowed?
New tableyes
New column with .optional()yes
New required column on an existing tableno
Make a required column optionalyes
Remove a column or table from the declarationyes: it stays in the database with its data until uninstall; a required column removed from a table you still declare becomes nullable
Make an optional column requiredno
Change a column's typeno
Rename a columnonly as remove + add: the new column must be optional, and the data is not copied

A change that breaks these rules is rejected with 422 and code DATA_SCHEMA_NOT_ADDITIVE. The response lists every violation, for example:

{
"error": "DATA_SCHEMA_NOT_ADDITIVE",
"message": "Data schema change is not additive: points.score: type_change (applied: double precision, declared: text)",
"details": {
"violations": [
{ "table": "points", "column": "score", "rule": "type_change",
"applied": "double precision", "declared": "text" }
]
}
}

rule is type_change, new_not_null_column or not_null_tightening. The check at upload compares against the tables as they are at that moment; approval checks again, and a version that fails there is not approved and gets the list as its review notes.

Storage quota​

Every extension has 1 GiB of data storage, shared by all accounts that install it. The used size counts every table of the extension with its indexes, minus the space the database can reuse. It is measured every 15 minutes.

While the extension is over the quota, insert and patch fail with 507 EXTENSION_STORAGE_QUOTA ("extension storage quota exceeded"). query, get and delete keep working, so the extension can delete rows it no longer needs to get back under the quota.

Deleted rows count once the database has vacuumed the table, usually within minutes to an hour after the delete. The next measurement after that sees the space. Writes are allowed again when the used size is at or below 90 % of the quota, so an extension that just reached the quota has to free some room before it can write again. Indexes keep their full size until they are rebuilt, so delete well below the quota rather than just under it.

Retention​

Rows have no time-based expiry. They stay until the account uninstalls the extension: then that account's rows are deleted (immediately, or after the uninstall retention window), and the tables go with the last install.