Schema
Define your database schema using TypeScript with full type safety.
Defining tables
Use defineSchema() from @tthr/schema to define your tables:
import { defineSchema, text, integer, real, timestamp } from '@tthr/schema';
export default defineSchema({
users: {
email: text().notNull().unique(),
name: text(),
},
posts: {
title: text().notNull(),
content: text(),
authorId: text().notNull().references('users'),
},
});
Column types
Tether supports the following column types:
| Type | TypeScript | Description |
|---|---|---|
text() |
string |
Text/string values |
integer() |
number |
Integer numbers |
real() |
number |
Floating point numbers |
blob() |
Uint8Array |
Binary data |
timestamp() |
Date |
Date/time (ISO 8601 string) |
boolean() |
boolean |
True/false (stored as SQLite integer 0/1) |
json<T>() |
T |
JSON data with type parameter |
asset() |
string |
File reference (UUID linking to storage) |
Using JSON columns
Define complex nested data with type safety:
import { defineSchema, text, json } from '@tthr/schema';
interface UserPreferences {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
}
export default defineSchema({
users: {
email: text().notNull(),
preferences: json<UserPreferences>().default({
theme: 'light',
notifications: true,
language: 'en',
}),
},
});
Using asset columns
Store file references that link to Tether's storage system:
import { defineSchema, text, asset } from '@tthr/schema';
export default defineSchema({
posts: {
title: text().notNull(),
coverImage: asset(), // Stores asset UUID
attachments: json<string[]>(), // Multiple asset UUIDs
},
});
Column modifiers
Chain modifiers to add constraints:
| Modifier | Description |
|---|---|
.primaryKey() |
Mark as primary key (also sets notNull) |
.autoincrement() |
Auto-increment (integer columns only) |
.notNull() |
Disallow null values |
.unique() |
Enforce uniqueness constraint |
.default(value) |
Set a default value |
.references('table') |
Foreign key reference (auto-references _id) |
.onDelete(action) |
Foreign key delete behaviour: 'cascade', 'set null', 'restrict' |
.oneOf([...]) |
Constrain to a set of allowed values (text columns only) |
Timestamp modifiers
Timestamps have additional convenience modifiers:
| Modifier | Description |
|---|---|
.defaultNow() |
Auto-set to current timestamp on insert |
.onUpdate() |
Auto-update to current timestamp on modification |
export default defineSchema({
posts: {
title: text().notNull(),
createdAt: timestamp().defaultNow(),
updatedAt: timestamp().defaultNow().onUpdate(),
},
});
Foreign keys
Define relationships between tables using .references():
export default defineSchema({
users: {
email: text().notNull().unique(),
},
posts: {
title: text().notNull(),
authorId: text().notNull().references('users'),
},
comments: {
content: text().notNull(),
postId: text().notNull().references('posts').onDelete('cascade'),
authorId: text().references('users').onDelete('set null'),
},
});
Delete behaviours
| Action | Description |
|---|---|
'cascade' |
Delete child records when parent is deleted |
'set null' |
Set foreign key to null when parent is deleted |
'restrict' |
Prevent deletion if child records exist (default) |
Enum constraints
Use .oneOf() to restrict a text column to a set of allowed values. This generates a SQL CHECK constraint and narrows the TypeScript type to a union of string literals.
export default defineSchema({
serverMembers: {
serverId: text().notNull().references('servers').onDelete('cascade'),
userId: text().notNull().references('users').onDelete('cascade'),
role: text().oneOf(['owner', 'admin', 'moderator', 'member']).notNull(),
},
channels: {
serverId: text().notNull().references('servers').onDelete('cascade'),
name: text().notNull(),
type: text().oneOf(['text', 'announcements']).notNull(),
},
friends: {
requesterId: text().notNull().references('users'),
addresseeId: text().notNull().references('users'),
status: text().oneOf(['pending', 'accepted', 'declined', 'blocked']).notNull(),
},
});
The generated TypeScript type for role will be 'owner' | 'admin' | 'moderator' | 'member' instead of string, giving you compile-time safety. The database will also reject any values not in the list.
System columns
Tether automatically adds system columns to every table. You don't need to define these in your schema:
| Column | Description |
|---|---|
_id |
UUID primary key (auto-generated if not provided on insert) |
_createdAt |
ISO 8601 timestamp when the record was created |
_updatedAt |
ISO 8601 timestamp, auto-updated on every modification |
These columns are always available in your queries and are excluded from CreateInput types since they're managed automatically.
Generating types
Run tthr generate to create TypeScript types from your schema:
tthr generate
This creates tether/_generated/db.ts with typed interfaces for all your tables.