> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Turso & LibSQL

> Run UQL on Turso Cloud from edge runtimes, on the embedded Turso engine, or on libSQL.

Source: https://uql-orm.dev/turso

All three options are SQLite underneath, so entities, queries and [migrations](https://uql-orm.dev/migrations.md) 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 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](https://uql-orm.dev/entities/computed-fields.md#relation-aggregates) reads the ids of the rows it names first, on both drivers.

## Turso Cloud

```sh
npm install uql-orm @tursodatabase/serverless
```

```ts
import { 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](https://uql-orm.dev/querying/streaming.md) 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

```sh
npm install uql-orm @tursodatabase/database
```

```ts
import { 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](https://uql-orm.dev/querying/streaming.md) natively rather than buffering the full result set.

## libSQL

```sh
npm install uql-orm @libsql/client
```

```ts
import { 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:

```ts
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:

```ts
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

Built in on both, so [semantic search](https://uql-orm.dev/querying/semantic-search.md) 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](https://uql-orm.dev/querying/semantic-search.md#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

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.
