Kura
Early Access: Documentation is actively evolving and may change without notice.

Schema

Define your database tables in TypeScript as the source of truth for migrations, Lua refs, and ORM metadata.

Your schema is defined once in src/schema.ts. From that file, KuraDB generates:

  • SQL migrations
  • Lua row and insert/update types
  • kura.db.tables.<tableName> metadata
  • generated column refs such as kura.db.tables.users.id

Examples in this page use a users table only as an illustration. The actual generated names always come from your own schema.ts.

When you only need updated Lua autocomplete, run kuradb generate --types-only from the extracted release directory or your local resource root. That regenerates lib/schema.generated.lua without creating a migration or starting FiveM.

schema.generated.lua is the LuaLS-facing artifact. It contains:

  • row types such as UsersRow
  • insert/update shapes such as UsersInsert and UsersUpdate
  • generated table definitions under kura.db.tables
  • generated column refs under table.columnRefs and direct fields such as users.id

Columns

Use defineColumn to declare a column:

import { defineColumn } from './orm';

const id = defineColumn('id', 'uuid', { primaryKey: true });
const username = defineColumn('username', 'string');
const deletedAt = defineColumn('deleted_at', 'date', { nullable: true });
const createdAt = defineColumn('created_at', 'date', { defaultExpression: 'NOW()' });

The first argument is the SQL column name. The key you assign it to in the table object becomes the TypeScript/Lua field name.

Example:

createdAt: defineColumn('created_at', 'date')
passwordHash: defineColumn('password_hash', 'string')

That generates Lua-facing fields:

users.createdAt
users.passwordHash

Column types

KindPostgreSQLTypeScriptLua
'uuid'UUIDstringstring
'string'VARCHAR(255)stringstring
'number'INTEGERnumbernumber
'bigint'BIGINTbigintnumber
'boolean'BOOLEANbooleanboolean
'date'TIMESTAMPDatenumber
'json'JSONBobjecttable
'custom'TEXTunknownany

Column options

OptionTypeDescription
primaryKeybooleanMarks the column as PRIMARY KEY
nullablebooleanAllows NULL
defaultExpressionstringRaw SQL default such as 'NOW()'
encodefunctionTransform a value before writing
decodefunctionTransform a value after reading

Custom encode / decode

Use hooks for values that need transformation between TypeScript and SQL:

const metadata = defineColumn<Record<string, unknown>>('metadata', 'json', {
  nullable: true,
  encode: (value) => JSON.stringify(value),
  decode: (raw) => typeof raw === 'string' ? JSON.parse(raw) : raw,
});

const createdAt = defineColumn<Date>('created_at', 'date', {
  encode: (value) => value.toISOString(),
  decode: (raw) => new Date(raw as string),
});

Tables

Use defineTable to group columns:

import { defineTable, defineColumn } from './orm';

export const users = defineTable('public', 'users', {
  id: defineColumn('id', 'uuid', { primaryKey: true }),
  username: defineColumn('username', 'string'),
  email: defineColumn('email', 'string'),
  passwordHash: defineColumn('password_hash', 'string'),
  createdAt: defineColumn('created_at', 'date'),
  updatedAt: defineColumn('updated_at', 'date'),
  deletedAt: defineColumn('deleted_at', 'date', { nullable: true }),
});

Arguments:

  1. Schema name, usually 'public'
  2. SQL table name
  3. Object mapping Lua/TypeScript field names to column definitions

The primaryKey list is derived automatically from columns with primaryKey: true.

Schema

Group all tables into a schema export:

import { defineSchema, defineTable, defineColumn } from './orm';

export const users = defineTable('public', 'users', { /* ... */ });
export const posts = defineTable('public', 'posts', { /* ... */ });

export const schema = defineSchema('public', {
  users,
  posts,
});

The schema object is consumed by:

  • kuradb generate
  • kuradb generate --types-only
  • kuradb migrate
  • the Lua type generator
  • the Lua query builder metadata under kura.db.tables

Generated Lua usage

For the best Lua autocomplete and refactor safety, use the generated table definitions and column refs:

local users = kura.db.tables.users

local rows = kura.db.select({ users.id, users.createdAt, users.passwordHash })
  :from(users)
  :where(op.isNull(users.deletedAt))
  :await()

print(rows[1].createdAt)
print(rows[1].passwordHash)

Builder results use your Lua field names when schema metadata is available. If your SQL column is created_at, the builder exposes it as createdAt in returned rows.

String-based usage still works:

local rows = kura.db.select()
  :from('users')
  :where(op.isNull('deleted_at'))
  :await()

Generated metadata shape

The generated Lua artifact includes both string maps and richer column refs:

local users = kura.db.tables.users

users.columns.createdAt   -- "created_at"
users.columnRefs.createdAt.sqlName -- "created_at"
users.columnRefs.createdAt.luaName -- "createdAt"
users.createdAt           -- same generated column ref object

This is why both of these work:

op.eq(users.createdAt, os.time())
op.eq('created_at', os.time())

Generated refs also carry the Lua alias that the builder returns in result rows. If your schema defines createdAt = defineColumn('created_at', 'date'), then:

local users = kura.db.tables.users

local row = kura.db.select({ users.createdAt })
  :from(users)
  :single()
  :await()

print(row.createdAt)

The builder compiles the SQL column name created_at, then aliases it back to the Lua field name createdAt in the returned row.

Aliases vs raw SQL

Generated schema aliases such as users.createdAt are supported directly. Arbitrary SQL expressions or custom aliases are not supported through plain string column names. Use explicit raw SQL when you need expression-level control.

Full example

A complete src/schema.ts:

import { defineColumn, defineSchema, defineTable } from './orm';

export const characters = defineTable('public', 'characters', {
  id: defineColumn('id', 'uuid', { primaryKey: true }),
  ownerId: defineColumn('owner_id', 'uuid'),
  firstName: defineColumn('first_name', 'string'),
  lastName: defineColumn('last_name', 'string'),
  dateOfBirth: defineColumn('date_of_birth', 'date'),
  cash: defineColumn('cash', 'number'),
  bank: defineColumn('bank', 'number'),
  metadata: defineColumn('metadata', 'json', { nullable: true }),
  createdAt: defineColumn('created_at', 'date', { defaultExpression: 'NOW()' }),
  deletedAt: defineColumn('deleted_at', 'date', { nullable: true }),
});

export const vehicles = defineTable('public', 'vehicles', {
  id: defineColumn('id', 'uuid', { primaryKey: true }),
  ownerId: defineColumn('owner_id', 'uuid'),
  model: defineColumn('model', 'string'),
  plate: defineColumn('plate', 'string'),
  garage: defineColumn('garage', 'string'),
  fuel: defineColumn('fuel', 'number'),
  properties: defineColumn('properties', 'json', { nullable: true }),
});

export const schema = defineSchema('public', {
  characters,
  vehicles,
});

On this page