Fix: Turso Not Working — Connection Refused, Queries Returning Empty, or Embedded Replicas Not Syncing
Quick Answer
How to fix Turso database issues — libsql client setup, connection URLs and auth tokens, embedded replicas for local-first apps, schema migrations, Drizzle ORM integration, and edge deployment.
The Problem
Connecting to Turso fails with an authentication error:
import { createClient } from '@libsql/client';
const client = createClient({
url: 'libsql://my-database-myorg.turso.io',
authToken: process.env.TURSO_AUTH_TOKEN,
});
const result = await client.execute('SELECT 1');
// Error: AUTHORIZATION_FAILED: Token is not validOr queries return empty results even though data exists:
const users = await client.execute('SELECT * FROM users');
console.log(users.rows); // []
// But the Turso CLI shows data in the tableOr embedded replicas fail to sync:
Error: Sync failed: unable to open database fileOr the database works locally but fails on Cloudflare Workers or Vercel Edge:
Error: Dynamic require of "node:fs" is not supportedWhy This Happens
Turso is a distributed SQLite database built on libSQL. It uses an HTTP-based protocol for remote connections and supports local embedded replicas:
- Auth tokens are scoped and expire — Turso tokens can be scoped to specific databases and have expiration times. A token generated for one database won’t work with another. Expired tokens return authorization errors without a clear expiration message.
- Remote queries go through the HTTP protocol —
libsql://URLs connect over HTTPS to Turso’s edge network. Network issues, DNS resolution failures, or incorrect URLs result in connection errors that look similar to authentication failures. - Embedded replicas need a local file path — when using embedded replicas (
syncUrl+urlpointing to a local file), libSQL creates a local SQLite file and syncs from the remote. If the directory doesn’t exist or the process doesn’t have write permissions, the sync fails silently and queries return stale or empty results. - Edge runtimes have no filesystem — Cloudflare Workers, Vercel Edge, and Deno Deploy can’t use embedded replicas because they have no persistent local filesystem. Use the HTTP client (
@libsql/client/http) for edge environments.
Fix 1: Connect to Turso Correctly
npm install @libsql/client# Create a database with the Turso CLI
turso db create my-app-db
# Get the connection URL
turso db show my-app-db --url
# → libsql://my-app-db-myorg.turso.io
# Create an auth token
turso db tokens create my-app-db
# → eyJhbGciOiJ...
# For read-only tokens
turso db tokens create my-app-db --read-only
# For expiring tokens
turso db tokens create my-app-db --expiration 7d// Basic remote connection
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!, // libsql://my-db-org.turso.io
authToken: process.env.TURSO_AUTH_TOKEN!, // eyJhbGciOiJ...
});
// Verify connection
const result = await client.execute('SELECT 1 AS ok');
console.log(result.rows[0]); // { ok: 1 }
// Basic CRUD operations
// Create table
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Insert with parameters — always use parameters, never string interpolation
await client.execute({
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
args: ['Alice', '[email protected]'],
});
// Named parameters
await client.execute({
sql: 'INSERT INTO users (name, email) VALUES (:name, :email)',
args: { name: 'Bob', email: '[email protected]' },
});
// Select
const users = await client.execute('SELECT * FROM users');
for (const row of users.rows) {
console.log(row.id, row.name, row.email);
}
// Select with filtering
const user = await client.execute({
sql: 'SELECT * FROM users WHERE email = ?',
args: ['[email protected]'],
});Fix 2: Transactions and Batch Operations
// Transaction — atomic multi-statement execution
const tx = await client.transaction('write');
try {
await tx.execute({
sql: 'INSERT INTO orders (user_id, total) VALUES (?, ?)',
args: [1, 99.99],
});
const orderResult = await tx.execute('SELECT last_insert_rowid() AS id');
const orderId = orderResult.rows[0].id;
await tx.execute({
sql: 'INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)',
args: [orderId, 42, 2],
});
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
}
// Batch — multiple statements in a single round trip (more efficient)
const batchResult = await client.batch([
{
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
args: ['Charlie', '[email protected]'],
},
{
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
args: ['Diana', '[email protected]'],
},
'SELECT * FROM users ORDER BY id DESC LIMIT 2',
], 'write');
// batchResult is an array — one result per statement
const insertedUsers = batchResult[2].rows;
// Conditional batch (statements depend on previous results)
const conditionalBatch = await client.batch([
{
sql: 'SELECT id FROM users WHERE email = ?',
args: ['[email protected]'],
},
// This runs regardless — batches don't support conditional logic
// Use transactions for conditional operations
], 'read');Fix 3: Embedded Replicas (Local-First)
Embedded replicas keep a local SQLite copy that syncs from Turso:
import { createClient } from '@libsql/client';
// Embedded replica — local file + remote sync
const client = createClient({
url: 'file:./local-replica.db', // Local SQLite file
syncUrl: process.env.TURSO_DATABASE_URL!, // Remote Turso URL
authToken: process.env.TURSO_AUTH_TOKEN!,
syncInterval: 60, // Sync every 60 seconds
});
// Manual sync — call after writes or when you need fresh data
await client.sync();
// Reads hit the local file (fast, works offline)
const users = await client.execute('SELECT * FROM users');
// Writes go to the remote and then sync back
await client.execute({
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
args: ['Eve', '[email protected]'],
});
// Sync after write to update the local replica
await client.sync();When to use embedded replicas:
- Server-side apps (Node.js, Bun) where you want fast reads
- Desktop apps (Electron, Tauri) for offline-first capability
- Self-hosted servers that need local SQLite performance with remote backup
When NOT to use embedded replicas:
- Edge runtimes (Cloudflare Workers, Vercel Edge) — no filesystem
- Serverless functions — ephemeral containers lose the local file
- If you need real-time sync across multiple instances
Fix 4: Edge Runtime Compatibility
Use the HTTP-only client for edge environments:
// For Cloudflare Workers, Vercel Edge, Deno Deploy
import { createClient } from '@libsql/client/http';
// HTTP client — no filesystem dependency
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// Works exactly the same as the regular client
const users = await client.execute('SELECT * FROM users');Cloudflare Workers:
// src/index.ts — Cloudflare Worker
import { createClient } from '@libsql/client/http';
interface Env {
TURSO_DATABASE_URL: string;
TURSO_AUTH_TOKEN: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const client = createClient({
url: env.TURSO_DATABASE_URL,
authToken: env.TURSO_AUTH_TOKEN,
});
const result = await client.execute('SELECT * FROM users LIMIT 10');
return new Response(JSON.stringify(result.rows), {
headers: { 'Content-Type': 'application/json' },
});
},
};Next.js Edge API Route:
// app/api/users/route.ts
import { createClient } from '@libsql/client/http';
export const runtime = 'edge';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export async function GET() {
const result = await client.execute('SELECT * FROM users');
return Response.json(result.rows);
}Fix 5: Drizzle ORM Integration
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit// src/db/schema.ts — define schema
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
role: text('role', { enum: ['admin', 'user'] }).default('user'),
createdAt: text('created_at').default('CURRENT_TIMESTAMP'),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
body: text('body').notNull(),
authorId: integer('author_id')
.notNull()
.references(() => users.id),
publishedAt: text('published_at'),
});// src/db/index.ts — create Drizzle instance
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import * as schema from './schema';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export const db = drizzle(client, { schema });// src/db/queries.ts — type-safe queries
import { db } from './index';
import { users, posts } from './schema';
import { eq, like, desc } from 'drizzle-orm';
// Select all users
const allUsers = await db.select().from(users);
// Select with filter
const admins = await db.select().from(users).where(eq(users.role, 'admin'));
// Insert
const newUser = await db.insert(users).values({
name: 'Alice',
email: '[email protected]',
role: 'admin',
}).returning();
// Update
await db.update(users)
.set({ role: 'admin' })
.where(eq(users.email, '[email protected]'));
// Delete
await db.delete(users).where(eq(users.id, 1));
// Join
const postsWithAuthors = await db
.select({
postTitle: posts.title,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.orderBy(desc(posts.publishedAt));// drizzle.config.ts — for migrations
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
} satisfies Config;# Generate migration SQL
npx drizzle-kit generate
# Push schema directly (development)
npx drizzle-kit push
# Open Drizzle Studio (database browser)
npx drizzle-kit studioFix 6: Multi-Database and Branching
Turso supports per-tenant databases and database branching:
# Create a database per tenant
turso db create tenant-acme
turso db create tenant-globex
# Branch a database for testing/staging
turso db create my-app-staging --from-db my-app-production// Multi-tenant — create client per request
function getClientForTenant(tenantId: string) {
return createClient({
url: `libsql://${tenantId}-db-myorg.turso.io`,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
}
// Usage in an API handler
async function handleRequest(tenantId: string) {
const client = getClientForTenant(tenantId);
const users = await client.execute('SELECT * FROM users');
return users.rows;
}
// Group databases for shared auth tokens
// turso group create my-group
// turso db create my-db --group my-group
// Token created for the group works for all databases in itStill Not Working?
“Token is not valid” but the token was just created — check that the token matches the database. Tokens are scoped to a specific database or group. If you created the token for db-a but are connecting to db-b, it won’t work. Recreate the token: turso db tokens create <correct-db-name>. Also check that the environment variable doesn’t have extra whitespace or newline characters.
Queries return empty but data exists in the CLI — you might be connected to a different database. Verify the URL matches: turso db show <db-name> --url. Also check if the table name has different casing — SQLite is case-insensitive for table names but the schema might differ. Run SELECT name FROM sqlite_master WHERE type='table' to list tables.
Embedded replica sync fails — the directory for the local file must exist and be writable. file:./data/replica.db requires the ./data/ directory to exist. Create it before connecting. Also, the syncUrl must use the libsql:// or https:// protocol — file:// is only for the local path.
“Dynamic require of node:fs” on edge runtime — you’re importing @libsql/client instead of @libsql/client/http. The default import includes the embedded replica support which needs Node.js fs. Switch to import { createClient } from '@libsql/client/http' for edge runtimes.
For related database issues, see Fix: Drizzle ORM Not Working and Fix: Prisma Connection Pool Exhausted.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Neon Database Not Working — Connection Timeout, Branching Errors, or Serverless Driver Issues
How to fix Neon Postgres issues — connection string setup, serverless HTTP driver vs TCP, database branching, connection pooling, Drizzle and Prisma integration, and cold start optimization.
Fix: Upstash Not Working — Redis Commands Failing, Rate Limiter Not Blocking, or QStash Messages Lost
How to fix Upstash issues — Redis REST client setup, rate limiting with @upstash/ratelimit, QStash message queues, Kafka topics, Vector search, and edge runtime integration.
Fix: Convex Not Working — Query Not Updating, Mutation Throwing Validation Error, or Action Timing Out
How to fix Convex backend issues — query/mutation/action patterns, schema validation, real-time reactivity, file storage, auth integration, and common TypeScript type errors.
Fix: Kysely Not Working — Type Errors on Queries, Migration Failing, or Generated Types Not Matching Schema
How to fix Kysely query builder issues — database interface definition, dialect setup, type-safe joins and subqueries, migration runner, kysely-codegen for generated types, and common TypeScript errors.