Database (ctx.db)

Every Function gets ctx.db, a client for your project's collections. It has eight methods, the same in TypeScript and Dart, and it talks to the same database your apps use.

ctx.db ignores your access rules — your Function must check the user

Functions run with a trusted system identity, not as the person who called them. Read, write and delete rules apply to client SDKs only: inside a Function, ctx.db can read and change every record, including collections marked server-only. This is what lets triggers and crons do their work, since they have no user at all.

So when a Function acts for a user, call ctx.db.asUser() so their access rules apply (see Acting as the user). Or check ctx.auth.user_id (null when the call is anonymous) and scope every query and write to it yourself.
my-orders/index.ts
TypeScript
export async function handler(ctx) {
  const userId = ctx.auth.user_id
  if (!userId) {
    return { error: 'sign in required' }
  }

  // ctx.db sees every record, so scope the query to this user yourself.
  const { records, total } = await ctx.db.find('orders', { owner_id: userId }, 50)
  return { orders: records, total }
}

Acting as the user: asUser()

ctx.db.asUser() returns the same eight methods, but every call is made as the signed-in caller, with their own session. Your collection's access rules apply exactly as they do in your app: reads return only what that user may see, and writes they aren't allowed to make are refused.

my-orders/index.ts
TypeScript
export async function handler(ctx) {
  // Access rules apply: only the orders this user may read come back.
  const { records } = await ctx.db.asUser().find('orders', {}, 50)
  return { orders: records }
}

Only when someone signed in called the Function

asUser() works when a signed-in user called the Function from your app, or through another Function that passed their session along. Triggers, crons, the dashboard's Invoke button and anonymous calls have no user, so there asUser() throws. It never falls back to the elevated ctx.db.
main.dart
Dart
final db = ctx['db'] as Map<String, dynamic>;
final mine = (db['asUser'] as Function)() as Map<String, dynamic>;
final result = await (mine['find'] as Function)('orders', {}, 50);

Methods

get(id)

returns the record

Fetches one record by id. Throws with status 404 if it does not exist.

insert(collection, data)

returns the new record

Creates a record in a collection.

find(collection, filters = {}, limit = 20, options)

returns { records, total }

Returns matching records and the total number that match. Oldest first unless options sort them.

update(id, data, options)

returns the updated record

Merges data into one record, found by id. options.expectedRevision makes it conditional.

upsert(collection, match, data)

returns { record, created }

Updates the record that matches, or creates one if none does. created is true when it created.

delete(id)

returns nothing

Deletes one record by id.

deleteWhere(collection, filters)

returns { deleted }

Deletes every record matching filters and returns how many.

batch(operations)

returns { results }

Runs several writes in one transaction: all apply, or none do.

Records

A record comes back with your fields at the top level, plus system fields that start with $:

$idThe record's id. Pass it to update, delete, or a batch op.
$collectionThe collection the record belongs to.
$createdAtWhen the record was created.
$updatedAtWhen the record last changed.
$revisionIncreases on every change. Pass it as expectedRevision to update, or expected_revision in a batch, to make an update conditional.
$createdByWho created the record. Present only when set.

Filters

find and deleteWhere take filters as an object of field names and values. They are deliberately simple:

  • Exact matches only. Every key must equal its value, and all keys must match. There are no operators: no greater-than, no "in", no contains.
  • Compared as text. { qty: 5 } matches a stored 5, and { active: true } matches true. You can't match null or a nested object.
  • Top-level fields only. Names may contain letters, digits and underscores. A name like address.city is rejected with an error, never silently ignored.
  • created_by is special. It matches the record's creator, not a field in your data.

find

find
TypeScript
const { records, total } = await ctx.db.find('orders', { status: 'open' }, 50)
// records: up to 50 open orders, oldest first
// total:   how many open orders exist in all

limit is capped at 100

limit defaults to 20 and is capped at 100: asking for 500 returns at most 100 records. Use total and the offset option to page through more.

An optional fourth argument pages and sorts:

offsetSkip this many records first. Default 0.
orderBySort by created_at, updated_at or id (the record's own timestamps and id), or by any field name in your data.
orderDesctrue for newest or largest first. Default false.
find with options
TypeScript
const page2 = await ctx.db.find('orders', { status: 'open' }, 50, {
  offset: 50,
  orderBy: 'created_at',
  orderDesc: true,
})

update

update merges: the fields you pass are set, and every other field keeps its value. It finds the record by $id alone. Because ctx.db bypasses access rules, confirm the record belongs to the user before changing it:

cancel-order/index.ts
TypeScript
export async function handler(ctx) {
  const userId = ctx.auth.user_id
  if (!userId) return { error: 'sign in required' }

  const orderNo = ctx.request.body.order_no
  const { records } = await ctx.db.find('orders', { order_no: orderNo, owner_id: userId }, 1)
  if (records.length === 0) return { error: 'order not found' }

  const order = await ctx.db.update(records[0].$id, { status: 'cancelled' })
  return { order }
}

To make an update conditional, pass the $revision you read as expectedRevision. If the record changed since, nothing is written and it throws with error code revision_mismatch.

conditional update
TypeScript
const order = await ctx.db.get(orderId)
await ctx.db.update(order.$id, { status: 'shipped' }, { expectedRevision: order.$revision })

upsert

Updates the record that matches match, or creates one if nothing matches, and returns{ record, created }. If match finds more than one record, it throws with status 409 rather than guessing which one you meant.

upsert
TypeScript
const { record, created } = await ctx.db.upsert('counters', { name: 'orders' }, { value: 1 })

deleteWhere

Deletes every record that matches and returns { deleted: n }. Two safeguards apply: empty filters are refused, so you can't wipe a collection by accident, and append-only collections refuse bulk deletes entirely.

deleteWhere
TypeScript
const { deleted } = await ctx.db.deleteWhere('sessions', { owner_id: userId })

batch

batch runs a list of writes in one transaction. If any of them fails, none of them apply. Each operation is an object with a type:

insertcollection, data
updaterecord_id, data, and optionally expected_revision
deleterecord_id
upsertcollection, match, data. Its result has created: true when it inserted.

An update with expected_revision applies only if the record still has that revision. If someone changed it in the meantime, the whole batch rolls back and throws with error code revision_mismatch. Re-read the record and try again.

sell-item/index.ts
TypeScript
const { records } = await ctx.db.find('products', { sku: sku }, 1)
const product = records[0]

try {
  const { results } = await ctx.db.batch([
    {
      type: 'update',
      record_id: product.$id,
      data: { stock: product.stock - 1 },
      expected_revision: product.$revision,
    },
    { type: 'insert', collection: 'sales', data: { sku: sku, owner_id: userId } },
  ])
  return { results }
} catch (err) {
  if (err.errorCode === 'revision_mismatch') {
    return { error: 'stock changed, try again' }
  }
  throw err
}

For a single conditional write, update takes expectedRevision too.

Errors

Every method throws KoolbaseDatabaseException when the request fails: a collection or record that doesn't exist, a unique-constraint clash, a revision mismatch, and so on. In TypeScript it carries:

statusThe HTTP status the database returned.
errorCodeA machine-readable code when there is one, such as revision_mismatch.
operationWhich method failed: get, find, insert, update, upsert, delete, deleteWhere or batch.
collectionThe collection involved, when known.
recordIdThe record involved, for update and delete.
bareMessageThe error message on its own.

Dart

The same eight methods, with the same arguments. Options are maps, such as {'orderBy': 'name'}. In Dart, ctx is a map, so each method is looked up and called as a function. The access-rules warning above applies here too.

main.dart
Dart
Future<Map<String, dynamic>> handler(Map<String, dynamic> ctx) async {
  final auth = ctx['auth'] as Map<String, dynamic>;
  final userId = auth['user_id'] as String?;
  if (userId == null) return {'error': 'sign in required'};

  final db = ctx['db'] as Map<String, dynamic>;
  final result = await (db['find'] as Function)('orders', {'owner_id': userId}, 50);

  return {'orders': result['records'], 'total': result['total']};
}

In test mode

When a Function runs in test mode, ctx.db writes nothing. Each method returns a simulated result marked __test: true, and find returns no records. asUser() returns the same simulated methods.