YaDominiosYaDominios

What your site's database supports: transactions, triggers and concurrency

Updated: 2026-10-19 · YaDominios

Every YaDominios Cloud site on a paid plan has its own SQLite database, available in your code as env.DB. It supports triggers with RAISE(ABORT), foreign keys enforced by default, views, unique and partial indexes, CHECK, EXISTS, INSERT … SELECT and UPDATE … RETURNING. Transactions use env.DB.batch([...]): if one statement fails, the whole batch is rolled back. Standalone BEGIN and COMMIT aren't supported. The database runs one query at a time, so two simultaneous requests never overwrite each other: with UPDATE … WHERE … RETURNING or a UNIQUE index, only one wins. Per-database limits: 10 GB, 30 seconds per query, 100 parameters per query and 1,000 queries per request. Scheduled jobs and Durable Objects aren't available inside the platform.

Who this is for: anyone building an app on YaDominios Cloud — a person or an AI — who needs to know, before designing, what they can rely on the site's database to do. Everything on this page is verified by automated tests on the same engine that runs in production.

At a glance

QuestionAnswer
What database is it?SQLite, one per site, in your code as env.DB. No middle layer: your code talks to it directly.
Transactions?Yes, with env.DB.batch([...]): all or nothing.
Manual BEGIN / COMMIT?No. They're rejected. Use batch().
Triggers (CREATE TRIGGER)?Yes, with RAISE(ABORT, 'message'), inside the same transaction.
Foreign keys?Yes, enforced by default. No need for PRAGMA foreign_keys = ON.
Two requests at once?The database runs one query at a time. With UPDATE … RETURNING or UNIQUE, only one wins.
Scheduled jobs or Durable Objects?No, not inside the platform. See below.

Transactions: a batch is all or nothing

Group the writes that must happen together in a batch(). If any of them fails, all of them are rolled back, including the ones that already ran. We tested it with a five-statement batch where the third one fails: nothing is saved. The same five statements run one by one, without batch(), leave the first two saved.

// Create the order and reduce stock: both or neither.
await env.DB.batch([
  env.DB.prepare("insert into orders (id, product_id, quantity) values (?, ?, ?)").bind(id, product, 2),
  env.DB.prepare("update products set stock = stock - 2 where id = ? and stock >= 2").bind(product),
]);

What doesn't work: sending BEGIN TRANSACTION and COMMIT as separate statements. The database rejects them with an error asking you to use batch().

Triggers

CREATE TRIGGER works with BEFORE and AFTER on INSERT, UPDATE and DELETE. A trigger that calls RAISE(ABORT, 'message') cancels the statement, and anything the trigger already wrote is rolled back too: it runs in the same transaction. Your code receives the message inside the error.

create trigger delivered_order_is_final
before update of status on orders
when old.status = 'delivered'
begin
  select raise(abort, 'a delivered order cannot change status');
end;

Triggers can live in your repository's schema.sql: at publish time we keep BEGIN … END; blocks intact even though they contain semicolons.

Foreign keys

They're always enforced. A row pointing to a record that doesn't exist is rejected with FOREIGN KEY constraint failed. If a migration needs to break them briefly, use PRAGMA defer_foreign_keys = on: anything still broken when the transaction ends raises an error. PRAGMA foreign_key_check works too.

Two requests at the same time

Each site's database runs one query after another. Two requests touching the same row never overwrite each other: one waits for the other. To assign something that can only have one owner — a time slot, a seat, a driver — use a single statement that checks and claims at once:

const { results } = await env.DB
  .prepare("update drivers set available = 0 where id = ? and available = 1 returning id")
  .bind(driver)
  .all();
if (results.length === 0) {
  // Another order got there first: pick someone else.
}

We tested two simultaneous requests on the same record: one wins and the other gets zero rows. A UNIQUE index behaves the same way: of two inserts with the same value, one goes in and the other is rejected.

Keep in mind: since queries wait in line, a slow query delays the others on that database. Use indexes, keep frequent queries short, and if one part of your app writes a lot (live locations, for example), give it a separate database on the same site.

SQLite features

All of these are available and tested one by one:

  • CREATE VIEW
  • CREATE UNIQUE INDEX and partial indexes with WHERE
  • CHECK (a value that breaks the rule is rejected)
  • EXISTS and NOT EXISTS
  • INSERT … SELECT
  • UPDATE … RETURNING
  • datetime('now') (in UTC)

Limits

LimitValue
Size of each database10 GB
Query duration30 seconds
Parameters per query100
Columns per table100
Size of a row, string or blob2 MB
Length of a SQL statement100 KB
Queries per request to your site1,000

What your plan allows (total storage, number of databases and sites) is shown on the pricing page and in your dashboard.

Querying over HTTP is for maintenance

Besides env.DB, you can query the database from outside with the query API or with AI Access. That route runs one query per call, each in its own transaction, plus the round trip over the internet. Use it for checking data, migrations and support. For what your app does all the time, use env.DB from your site's code.

Scheduled jobs and real time

YaDominios Cloud doesn't run scheduled jobs and doesn't support your own Durable Objects inside the site. The approach that works:

  1. Do the work inside the request whenever you can: for example, assign at the moment the order comes in, with UPDATE … RETURNING inside a batch().
  2. Anything that needs a clock (expiring an offer, retrying every minute) is triggered by an external service that calls a route on your site over the internet, protected by a secret you store in the dashboard variables.
  3. So two calls never do the same work at once, keep a "lock" row and claim it with UPDATE … WHERE expires < ? RETURNING: only one call gets it.

The details are in the Publish on YaDominios Cloud guide.

Frequently asked questions

Can I use BEGIN and COMMIT?

No. Group your statements in env.DB.batch([...]): if one fails, all of them are rolled back.

Do I need to turn on foreign keys?

No. They're always enforced, no PRAGMA needed.

Can two customers buy the last item at the same time?

Not if you decrement it with a single statement that checks and decrements at once (UPDATE … WHERE stock >= 1 RETURNING). The database runs one query at a time and only one of the two succeeds.

Do triggers work?

Yes, BEFORE and AFTER, with RAISE(ABORT) to cancel. Anything the trigger wrote is rolled back along with the statement.

Keep reading

Ready for your domain?

Search for it now and have it live in minutes.

Search my domain