Turso and libSQL: the database that fits the Solana stack
Turso is a distributed SQLite-compatible database with a Rust core. It's the natural off-chain storage layer for Solana apps — edge-deployable, low-latency, SQLite-familiar, and built on the same performance-first philosophy as the chain itself.
Every Solana app eventually runs into the same problem: the chain is great at storing state that needs to be trusted, verified, and settled — but it's a poor fit for everything else. User profiles, off-chain indexes, cached RPC responses, search queries, leaderboards, analytics. These belong in a database, not an account.
The database that fits the Solana stack best in 2026 is Turso — not because of marketing, but because it shares the same design values: Rust core, zero-overhead primitives, edge-deployable, and fast enough that you stop thinking about latency.
What Turso is
Turso is a managed database service built on libSQL — a fork of SQLite maintained by the Turso team. The fork adds three things SQLite alone doesn't have:
- Remote replication. Sync a local embedded SQLite file to a Turso cloud database. Reads are local (zero network latency); writes propagate to the cloud. This is the embedded replica pattern — the most underrated feature in the database.
- HTTP and WebSocket protocols. The Turso server (
sqld) exposes a JSON HTTP API (/v2/pipeline) alongside the native WebSocket protocol. This is what makes it usable from Cloudflare Workers and Pages — runtimes where the standard@libsql/clientpackage fails because it depends onXMLHttpRequest. - Multi-tenancy and branching. Create per-user or per-environment databases in seconds. Useful for SaaS Solana apps that need data isolation.
The server (sqld) is written in Rust. The libSQL library itself is a C codebase (SQLite's heritage) with a first-class Rust crate wrapping it. If you're a Solana developer, you already live in Rust — Turso meets you there.
The Rust connection
Solana chose Rust for the same reasons Turso's team chose it for sqld: no garbage collector, predictable latency, memory safety without a runtime, and a toolchain that makes correctness the default path rather than a discipline.
This matters practically. A GC pause in a database server causes the same problem as a GC pause in a validator — it breaks latency guarantees at the worst time. The Rust implementation of sqld avoids that class of failure at the same layer Agave avoids it in the consensus path.
For Solana programs that need an off-chain backend, the libsql crate is the native Rust client. No FFI boilerplate, full async support via Tokio:
# Cargo.toml
[dependencies]
libsql = "0.6"
tokio = { version = "1", features = ["full"] }use libsql::{Builder, params};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db = Builder::new_remote(
std::env::var("TURSO_URL")?,
std::env::var("TURSO_TOKEN")?,
)
.build()
.await?;
let conn = db.connect()?;
// Store off-chain metadata for a Solana wallet
conn.execute(
"INSERT OR REPLACE INTO profiles (pubkey, display_name, updated_at)
VALUES (?1, ?2, strftime('%s', 'now'))",
params![
"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"metasal"
],
)
.await?;
let mut rows = conn
.query("SELECT display_name FROM profiles WHERE pubkey = ?1", params!["9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"])
.await?;
while let Some(row) = rows.next().await? {
println!("{}", row.get::<String>(0)?);
}
Ok(())
}From Cloudflare Workers and Pages
The @libsql/client npm package works in Node.js. It does not work on Cloudflare Workers or Pages — those runtimes don't implement XMLHttpRequest, which the WebSocket client depends on internally.
The fix is simple: skip the SDK and call the HTTP pipeline API directly. This is what devrels.xyz does — every query runs through a thin fetch wrapper:
// lib/turso.ts — works on CF Pages edge runtime
async function tursoExecute(
sql: string,
args: { type: string; value: string | number | null }[] = []
) {
const url = process.env.TURSO_DATABASE_URL!.replace("libsql://", "https://")
const token = process.env.TURSO_AUTH_TOKEN!
const res = await fetch(`${url}/v2/pipeline`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
requests: [
{ type: "execute", stmt: { sql, args } },
{ type: "close" },
],
}),
})
const data = await res.json() as any
return data.results?.[0]?.response?.result?.rows ?? []
}
// Usage
const rows = await tursoExecute(
"SELECT name, avatar FROM devs WHERE LOWER(twitter) = LOWER(?) LIMIT 1",
[{ type: "text", value: "metasal" }]
)The pipeline protocol batches multiple statements in one HTTP round trip. Pair a close request at the end to release the connection — the pattern above is the minimal correct form. If you need the named column map rather than raw row arrays, parse result.cols alongside result.rows:
const result = data.results?.[0]?.response?.result
const columns = result.cols.map((c: { name: string }) => c.name)
const rows = result.rows.map((row: any[]) =>
Object.fromEntries(columns.map((col: string, i: number) => [col, row[i]?.value]))
)Architecture for a Solana app
A typical Solana dapp has two kinds of state. Keeping them separate is the right call — don't fight the tools:
On-chain (Solana accounts)
├── Token balances
├── Program state (escrows, DAOs, NFT ownership)
└── Settlement / finality
Off-chain (Turso)
├── User profiles (display name, avatar, linked socials)
├── Cached RPC responses (token prices, account snapshots)
├── Indexing (transaction history enriched with metadata)
├── Analytics (daily active wallets, volume by program)
└── Leaderboards (contest scores, staking tiers)The backend layer between them is typically a Next.js edge function or a Rust axum server. Either way, Turso is the read/write target for everything that doesn't need on-chain finality.
Embedded replicas for local dev
The most underused Turso feature is the embedded replica: a local SQLite file that syncs from (and writes to) a remote Turso database.
import { createClient } from "@libsql/client"
// Local-first: reads hit the local file, syncs every 60s
const db = createClient({
url: "file:./local.db",
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
syncInterval: 60,
})
await db.sync() // force a sync on startupIn local development this means your Solana indexer reads from a local SQLite file — zero latency, no network dependency, no rate limits. In production (Node.js), point at the remote URL directly. On Cloudflare edge (no filesystem), use the HTTP wrapper above. Three environments, one schema.
Indexing Solana transaction history
One of the best fits for Turso in a Solana app is storing parsed transaction data — enriched, queryable, without hammering getSignaturesForAddress on every page load. A simple schema:
CREATE TABLE transactions (
signature TEXT PRIMARY KEY,
block_time INTEGER NOT NULL,
wallet TEXT NOT NULL,
program TEXT NOT NULL,
type TEXT, -- 'swap', 'transfer', 'stake', etc.
amount_sol REAL,
meta TEXT -- JSON blob for type-specific fields
);
CREATE INDEX idx_wallet_time ON transactions (wallet, block_time DESC);
CREATE INDEX idx_program ON transactions (program);A background worker (a Yellowstone gRPC subscriber, a Helius webhook handler, or a simple poller) writes into this table as transactions land. Your API reads back fast, filtered, sorted queries without touching the RPC at all. Combine with Yellowstone gRPC streaming to get real-time writes.
Why not Postgres?
For most Solana side-data use cases, Postgres is overpowered. You don't need connection pooling, a separate schema migration tool, or the operational overhead of a running process. SQLite is a file; Turso makes that file distributed and consistent. The question of when to graduate to Postgres has a clean answer: when you're doing joins across 10M+ rows or need advanced SQL features (window functions, JSON path operations, etc.) at scale. Until then, Turso runs fine on a free tier with sub-5ms reads from the edge.
That said, if you're already on Postgres and it's working, there's no reason to migrate. This is a starting-point recommendation, not a migration pitch.
Getting started
# Install the CLI
brew install tursodatabase/tap/turso
# Auth
turso auth login
# Create a database
turso db create my-solana-app
# Get the URL and token
turso db show my-solana-app --url
turso db tokens create my-solana-appPaste those into your .env.local as TURSO_DATABASE_URL and TURSO_AUTH_TOKEN. On Cloudflare Pages, set them as encrypted environment secrets in the dashboard — the same names, same values.
References
Keep reading
Most Solana streaming assumes the consumer is somewhere else on the network, so it serializes and ships bytes over gRPC. But a lot of infra — RPC sidecars, indexers, MEV bots — runs on the same host as the validator. Qlaster is a Rust system that fans account/tx updates over shared-memory rings instead of sockets: zero-copy, eventfd-driven, SCM_RIGHTS fd passing. A look at the design.
Don't grind keypairs. Grind seeds. caveman's `vanity` tool generates Solana vanity addresses via CreateAccountWithSeed at GPU speed — here's why it's so much faster and what you give up.
Benchmarked: Solana Foundation public RPC, Extrnode, and PublicNode across getSlot, getLatestBlockhash, getBalance, getEpochInfo, getSignaturesForAddress, and getTokenAccountsByOwner. The basic methods are similar. The indexing-heavy methods diverge by 4.5×. Published RPC 2.0 numbers push that to 24×.