Skip to content
NewFrom Mongoose to SQL: migrating MongoDB to PostgreSQL7 min read

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/serverless uql-orm/turso Turso Cloud over fetch(), including 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 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.

Terminal window
npm install uql-orm @tursodatabase/serverless
import { 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.

Terminal window
npm install uql-orm @tursodatabase/database
import { 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.

Terminal window
npm install uql-orm @libsql/client
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, 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',
}),
);

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

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.

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.