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/serverless |
uql-orm/turso |
Turso Cloud over pure fetch(). No native dependency, so it runs on Cloudflare Workers and Vercel Edge. |
@tursodatabase/database |
uql-orm/turso/local |
The embedded Rust engine, for local-first and desktop apps. |
@libsql/client |
uql-orm/libsql |
Existing libSQL/sqld databases, including embedded replicas. |
Turso Cloud runs Turso Database, the ground-up Rust rewrite of SQLite. libSQL is the earlier fork of SQLite’s C source, still maintained; reach for uql-orm/libsql when you already run it.
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 client is built on first use rather than in the constructor, so a pool can sit at module scope in a Worker without loading the driver 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.
The pool also accepts an already-built client, which covers @libsql/client/web, @libsql/client-wasm, or a test double. Any client with the same execute / transaction / close shape works, and you keep ownership of its lifecycle: pool.end() will not close a client you injected.
import { createClient } from '@tursodatabase/serverless/compat';import { TursoQuerierPool } from 'uql-orm/turso';
const pool = new TursoQuerierPool(createClient({ url, authToken }));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 options (readonly, fileMustExist, timeout). 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,});Vector search
Section titled “Vector search”Built in on both, so semantic search needs no extension: cosine and l2 everywhere, plus inner on Turso’s Rust engine. Neither exposes an ANN index to UQL yet, so a $vector sort scans the table and computes each distance.
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.