Database API
Query and mutate data using the type-safe database client.
Overview
The db object provides a Prisma-style API for interacting with your database. It's passed to your function handlers and provides full type safety based on your schema.
import { query, z } from '../_generated/db';
export const list = query({
args: z.object({ limit: z.number().optional() }),
handler: async ({ args, db }) => {
return db.users.findMany({ limit: args.limit });
},
});
Reading data
findMany
Retrieve multiple rows from a table. Optionally filter and sort results.
// Get all rows
const allPosts = await db.posts.findMany();
// Filter with where clause
const published = await db.posts.findMany({
where: { published: true },
});
// Filter with multiple conditions
const userPosts = await db.posts.findMany({
where: {
authorId: 'user-123',
published: true,
},
});
// Sort results
const recent = await db.posts.findMany({
orderBy: { createdAt: 'desc' },
});
// Limit and offset for pagination
const page = await db.posts.findMany({
limit: 10,
offset: 20,
});
findFirst
Retrieve the first row matching the criteria, or null if none found.
const admin = await db.users.findFirst({
where: { role: 'admin' },
});
const latest = await db.posts.findFirst({
where: { authorId: 'user-123' },
orderBy: { createdAt: 'desc' },
});
findById
Retrieve a single row by its primary key. Returns null if not found.
const user = await db.users.findById('user-123');
const post = await db.posts.findById(42);
if (!user) {
throw new Error('User not found');
}
count
Count rows matching the criteria.
const total = await db.posts.count();
const published = await db.posts.count({
where: { published: true },
});
Writing data
insert
Insert a new row and return the created record.
const newUser = await db.users.insert({
id: 'user-456',
email: '[email protected]',
name: 'Alice',
});
const post = await db.posts.insert({
title: 'Hello World',
content: 'My first post',
authorId: 'user-456',
});
insertMany
Insert multiple rows in a single operation.
const newTags = await db.tags.insertMany([
{ name: 'typescript' },
{ name: 'sqlite' },
{ name: 'realtime' },
]);
update
Update rows matching the criteria and return the count of affected rows.
const updated = await db.posts.update({
where: { _id: 'post-abc-123' },
data: {
title: 'Updated Title',
updatedAt: new Date(),
},
});
await db.posts.update({
where: { authorId: 'user-123' },
data: { published: false },
});
upsert
Insert a row or update it if it already exists. The where clause determines which field to match on — you can upsert on any field, not just the primary key.
const user = await db.users.upsert({
where: { email: '[email protected]' },
create: {
id: 'user-789',
email: '[email protected]',
name: 'Bob',
},
update: {
name: 'Bob Updated',
lastLoginAt: new Date(),
},
});
How it works: The upsert uses
findFirstinternally to check if a record matching thewhereclause exists. If found, it updates that record; if not, it creates a new one.
delete
Delete rows matching the criteria and return the count of deleted rows.
const deleted = await db.posts.delete({
where: { _id: 'post-abc-123' },
});
await db.sessions.delete({
where: { expiresAt: { lt: new Date() } },
});
deleteMany
Delete multiple rows matching the criteria.
const deleted = await db.posts.deleteMany({
where: { authorId: 'user-123' },
});
console.log(`Deleted ${deleted} posts`);
deleteAll
Delete all rows in a table. Use with caution!
const deleted = await db.tempData.deleteAll();
deleteById
Delete a single row by its primary key.
const deleted = await db.posts.deleteById(42);
if (!deleted) {
throw new Error('Post not found');
}
System columns
Every table automatically includes system columns that Tether manages for you:
| Column | Description |
|---|---|
_id |
UUID primary key, auto-generated on insert if not provided |
_createdAt |
ISO 8601 timestamp, auto-set when record is created |
_updatedAt |
ISO 8601 timestamp, auto-updated on every modification |
const post = await db.posts.insert({
title: 'Hello World',
});
console.log(post._id); // e.g. '550e8400-e29b-41d4-a716-446655440000'
Return values
| Method | Returns |
|---|---|
findMany |
Array of records (may be empty) |
findFirst / findById |
Single record or null |
count |
Number of matching records |
insert |
The created record with all fields |
insertMany |
Array of created records |
update |
Number of affected rows |
upsert |
The created or updated record |
delete |
Number of deleted rows |
deleteMany |
Number of deleted rows |
deleteAll |
Number of deleted rows |
deleteById |
Boolean indicating success |
Where clauses
Filter data using various comparison operators:
| Syntax | Description |
|---|---|
{ field: value } |
Equals |
{ field: { ne: value } } |
Not equals |
{ field: { gt: value } } |
Greater than |
{ field: { gte: value } } |
Greater than or equal |
{ field: { lt: value } } |
Less than |
{ field: { lte: value } } |
Less than or equal |
{ field: { in: [...] } } |
In array |
{ field: { notIn: [...] } } |
Not in array |
{ field: { contains: 'str' } } |
Contains substring |
{ field: { startsWith: 'str' } } |
Starts with |
{ field: { endsWith: 'str' } } |
Ends with |
{ field: null } |
Is null |
{ field: { ne: null } } |
Is not null |
Combining conditions
// AND (default — all conditions must match)
const results = await db.posts.findMany({
where: {
published: true,
authorId: 'user-123',
},
});
// OR
const results = await db.posts.findMany({
where: {
OR: [
{ status: 'draft' },
{ status: 'review' },
],
},
});
// Complex conditions
const results = await db.posts.findMany({
where: {
published: true,
OR: [
{ title: { contains: 'TypeScript' } },
{ tags: { contains: 'typescript' } },
],
},
});
Ordering
Sort results using orderBy:
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' },
});
const posts = await db.posts.findMany({
orderBy: [
{ published: 'desc' },
{ createdAt: 'desc' },
],
});
Including related data
Use include to fetch related records along with your query results. Tether automatically detects relationships through foreign keys and supports arbitrary nesting depth.
Belongs-to (many-to-one)
When your table has a foreign key column pointing to another table, include returns the related record as a nested object.
// Posts table has an "authorId" column referencing "users"
const posts = await db.posts.findMany({
include: { users: true },
});
// Result: [{ _id: '...', title: '...', authorId: 'user-1', author: { _id: 'user-1', name: 'Alice' } }]
The relation key is derived from the foreign key column — authorId becomes author, categoryId becomes category. You can also include by relation key:
const posts = await db.posts.findMany({
include: { author: true },
});
Has-many (one-to-many)
When another table has a foreign key pointing back to your table, include returns an array of related records.
// Comments table has a "postId" column referencing "posts"
const posts = await db.posts.findMany({
include: { comments: true },
});
// Result: [{ _id: '...', title: '...', comments: [{ _id: '...', text: '...', postId: '...' }] }]
Nested includes
Include related data at any depth by passing an object with its own include:
// Posts → comments → likes (3 levels deep)
const posts = await db.posts.findMany({
include: {
comments: {
include: {
likes: true,
},
},
},
});
// Posts → author + comments → user (mixed directions)
const posts = await db.posts.findMany({
include: {
author: true,
comments: {
include: {
user: true,
},
},
},
});
Note: Nesting is limited to 5 levels deep to prevent runaway queries. Each include level runs a separate batched query, so performance scales well even with many related records.
Include with other options
Includes work alongside all other query options:
const posts = await db.posts.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
limit: 10,
include: {
author: true,
comments: true,
},
});
Includes also work with findFirst, findUnique, and findById:
const post = await db.posts.findById('post-123', {
include: { comments: { include: { likes: true } } },
});
Pagination
Use limit and offset for pagination:
const pageSize = 20;
const page = 3;
const posts = await db.posts.findMany({
orderBy: { createdAt: 'desc' },
limit: pageSize,
offset: (page - 1) * pageSize,
});
const total = await db.posts.count();