Turso & LibSQL
Turso Cloud, the embedded Turso engine and libSQL are all SQLite underneath, so entities, queries and migrations are the same on each. Only the pool changes.
| Package | Entry point | Use it for |
|---|---|---|
@tursodatabase/ |
uql-orm/ |
Turso Cloud over fetch(), including Cloudflare Workers and Vercel Edge |
@tursodatabase/ |
uql-orm/ |
The embedded Rust engine, for local-first and desktop apps |
@libsql/ |
uql-orm/ |
libSQL and sqld databases, embedded replicas, and edge clients |
A Turso Cloud database runs libSQL (a fork of SQLite’s C source) unless it was created as tursodb, which runs the Rust engine, so uql-orm/turso emits only SQL both accept.
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,});The driver uses fetch(), has no native binary and loads on the first query, so the pool can sit at module scope in a Worker. Keep uql-orm/turso/local out of edge bundles. Each querier gets its own server session, so queriers never wait on each other, and streaming reads rows as the server produces them. Driver options such as requestHeaders pass through.
Embedded Turso
Section titled “Embedded Turso”npm install uql-orm @tursodatabase/databaseimport { TursoLocalQuerierPool } from 'uql-orm/turso/local';
const pool = new TursoLocalQuerierPool('app.db');It defaults to ':memory:', takes the engine’s own options second (readonly, timeout, encryption and the rest), and streams natively.
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, migrations must reach the remote, or the next sync loses the DDL. Pass syncUrl and the migrator connects there for schema changes:
const pool = new LibsqlQuerierPool({ url: 'file:./local.db', syncUrl: process.env.LIBSQL_SYNC_URL, authToken: process.env.LIBSQL_AUTH_TOKEN,});You can also pass a client you built, such as @libsql/client/web or @libsql/client-wasm. Every querier shares it, and pool.end() leaves it open.
import { createClient } from '@libsql/client/web';
const pool = new LibsqlQuerierPool( createClient({ url: process.env.TURSO_DATABASE_URL!, authToken: process.env.TURSO_AUTH_TOKEN, intMode: 'bigint', }),);Vector search
Section titled “Vector search”Vectors are built in, so semantic search needs no extension. A metric the engine lacks throws:
cosine, l2 |
inner |
Vector index | |
|---|---|---|---|
| libSQL | yes | no | DiskANN |
| Turso Cloud | yes | no | DiskANN on libSQL; refused on tursodb |
| Embedded Turso | yes | yes | none: a plain index, and searches scan |
Good to know
Section titled “Good to know”SQLite-family indexes take no USING clause, so UQL drops an index type such as hnsw and an entity written for Postgres migrates unchanged. The Rust engine cannot read the table a write changes from inside a subquery, so on both Turso drivers an updateMany or deleteMany filtered by a relation first reads the matching ids. Neither libSQL nor Turso orders json_group_array, so a populated to-many keeps the subquery’s order.
What the ORM adds on top of the driver
Section titled “What the ORM adds on top of the driver”Measured on the embedded engine against @tursodatabase/database directly, with the same prepared SQL:
| 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, in-memory, Node 24 on an M4 Pro. UQL adds tens of microseconds for building SQL and hydrating entities, which an HTTP round trip to Turso Cloud dwarfs.