Turso & LibSQL
All three options are SQLite underneath, so entities, queries and migrations are identical across them. Only the pool changes.
| Package | Entry point | Use it for |
|---|---|---|
@tursodatabase/ |
uql-orm/ |
Turso Cloud over pure fetch(). No native dependency, so it runs on Cloudflare Workers and Vercel Edge. |
@tursodatabase/ |
uql-orm/ |
The embedded Rust engine, for local-first and desktop apps. |
@libsql/ |
uql-orm/ |
Existing libSQL/sqld databases, including embedded replicas and clients built for the edge. |
Turso Database is the ground-up Rust rewrite of SQLite; libSQL is the earlier fork of SQLite’s C source, still maintained. A Turso Cloud database runs libSQL unless it was created as tursodb, which runs the Rust engine. uql-orm/turso reaches either, so it emits only SQL both accept. The Rust engine cannot read the table a write changes from inside a subquery, so an updateMany or deleteMany filtered by a relation or a relation aggregate reads the ids of the rows it names first, on both drivers.
Turso Cloud
Section titled “Turso Cloud”npm install uql-orm @tursodatabase/serverlessimport { TursoQuerierPool } from 'uql-orm/turso';
const pool = new TursoQuerierPool({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN,});
const todos = await pool.findMany(Todo, { $select: { id: true, title: true }, $limit: 50,});The driver is loaded on first use rather than in the constructor, so a pool can sit at module scope in a Worker without loading it until a request needs it. Nothing extra is required in an edge runtime: the driver speaks HTTP through fetch() and pulls in no native binary. Import only uql-orm/turso in an edge bundle; uql-orm/turso/local is a separate entry point precisely because it reaches for binaries that do not resolve on Workers.
Every querier opens a session of its own, one stream on the server, so queriers never wait on each other and a transaction is plain BEGIN/COMMIT on its stream. Streaming reads the rows off the statement’s cursor as the server steps it. The settings are the driver’s own, so requestHeaders (for routing through a gateway) and defaultQueryTimeout apply too.
Embedded Turso
Section titled “Embedded Turso”npm install uql-orm @tursodatabase/databaseimport { TursoLocalQuerierPool } from 'uql-orm/turso/local';
const pool = new TursoLocalQuerierPool('app.db');Pass ':memory:' for an ephemeral database; the second argument takes the engine’s own options (readonly, timeout, encryption, experimental and the rest). This driver supports streaming natively rather than buffering the full result set.
libSQL
Section titled “libSQL”npm install uql-orm @libsql/clientimport { LibsqlQuerierPool } from 'uql-orm/libsql';
const pool = new LibsqlQuerierPool({ url: process.env.LIBSQL_URL!, authToken: process.env.LIBSQL_AUTH_TOKEN,});For an embedded replica (a local file synced from a remote), migrations must run against the remote so DDL is not lost on the next sync. Give UQL both URLs and the migrator opens its own connection to syncUrl for schema changes:
const pool = new LibsqlQuerierPool({ url: 'file:./local.db', syncUrl: process.env.LIBSQL_SYNC_URL, authToken: process.env.LIBSQL_AUTH_TOKEN,});The pool also takes a client you built, such as @libsql/client/web for an edge runtime or @libsql/client-wasm, and shares it with every querier. It is yours, so pool.end() leaves it open:
import { createClient } from '@libsql/client/web';import { LibsqlQuerierPool } from 'uql-orm/libsql';
const pool = new LibsqlQuerierPool( createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN, }),);Vector search
Section titled “Vector search”Built in on both, so semantic search needs no extension: cosine and l2 everywhere, plus inner on the embedded engine. A Turso Cloud database may run libSQL, which has no dot product, so uql-orm/turso refuses inner while building the query rather than sending a call the server lacks. On libSQL, a vector @Index is a DiskANN index that ranked, paged searches read. The Rust engine has none: a tursodb database refuses the index, and the embedded engine builds it plain and scans. See vector indexes.
The distance functions are named differently on each engine (vector_distance_cos here, vec_distance_cosine on SQLite with sqlite-vec), and UQL emits the right one per dialect. Ask for a metric an engine lacks and it throws while building the query rather than sending a call to a function that is not there.
CREATE INDEX has no USING clause anywhere in the SQLite family, so an index declaring a type (hnsw on an entity written for Postgres, or a plain btree) would be a syntax error. UQL drops the clause and emits a plain index, so the entity migrates unchanged.
What the ORM costs on top of the driver
Section titled “What the ORM costs on top of the driver”The embedded engine runs in-process, which makes it the honest place to measure what UQL adds over calling @tursodatabase/database directly. Prepared statements, same SQL, same database:
| Operation | Raw driver | Through UQL | Difference |
|---|---|---|---|
| Read by primary key | 3.4 us | 19.4 us | +16 us |
| Insert 10 rows | 128 us | 157 us | +29 us |
| Filtered top-10 over ~11k rows | 2.91 ms | 2.99 ms | +0.08 ms |
Median of 5 runs of 200 iterations after 30 warmup iterations, in-memory database seeded with 1,000 rows, Node 24 on an M4 Pro. The driver hands back raw row arrays and UQL hands back hydrated entities, so the difference covers building the SQL, binding, and mapping rows to objects.
Read it in absolute terms. The ORM costs tens of microseconds per operation, whatever the query. The percentage is a property of the query, not of UQL: that same 16 us is 460% of a 3 us primary-key read and 3% of a 3 ms scan. On Turso Cloud every statement is an HTTP round trip, so it disappears.