All articles
solanarustinfrastructuredatabasetooling

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.

Share

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/client package fails because it depends on XMLHttpRequest.
  • 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:

toml
# Cargo.toml
[dependencies]
libsql = "0.6"
tokio = { version = "1", features = ["full"] }
rust
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:

typescript
// 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:

typescript
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:

text
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.

typescript
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 startup

In 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:

sql
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

bash
# 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-app

Paste 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