Turso - Kompletny Przewodnik po Edge SQLite Database
Czym jest Turso?
Turso to rozproszona SQLite database zaprojektowana dla nowoczesnych aplikacji edge. Wykorzystuje libSQL - otwarty fork SQLite rozwijany przez zespół Turso - który dodaje funkcje takie jak replikacja, HTTP/WebSocket dostęp i rozszerzenia, zachowując pełną kompatybilność z SQLite.
Główna innowacja Turso to możliwość umieszczenia replik bazy danych blisko użytkowników na całym świecie (edge locations), co drastycznie redukuje latencje. Dodatkowo Turso oferuje embedded replicas - lokalną kopię bazy w aplikacji, która synchronizuje się z główną bazą, zapewniając mikrosekundowe czasy odczytu.
Dlaczego Turso?
Kluczowe zalety Turso
- Edge locations - Repliki na całym świecie, blisko użytkowników
- Embedded replicas - Lokalna kopia bazy w aplikacji
- libSQL - Otwarty fork SQLite z dodatkowymi funkcjami
- Ultra-niskie latencje - Mikrosekundy dla lokalnych odczytów
- SQLite compatibility - Pełna kompatybilność z SQLite
- Serverless - Automatyczne skalowanie, brak zarządzania
- Cost-effective - Znacznie tańszy niż PostgreSQL/MySQL
- Simple - Jeden plik = cała baza danych
Turso vs Inne Bazy Danych
| Cecha | Turso | PlanetScale | Neon | D1 (Cloudflare) |
|---|---|---|---|---|
| Engine | SQLite/libSQL | MySQL/Vitess | PostgreSQL | SQLite |
| Edge locations | 30+ | 10+ | 10+ | Cloudflare PoPs |
| Embedded replicas | Tak | Nie | Nie | Nie |
| Local latency | ~0.5ms | ~50ms | ~50ms | ~5ms |
| Free tier | 9GB | 5GB | 3GB | 5GB |
| Branching | Tak | Tak | Tak | Nie |
| Open source | libSQL (tak) | Nie | Nie | Nie |
Turso vs SQLite
| Cecha | Turso | SQLite |
|---|---|---|
| Replikacja | Automatyczna | Brak |
| Edge deployment | Tak | Nie |
| HTTP access | Tak | Nie |
| Websockets | Tak | Nie |
| Skalowanie | Automatyczne | Manualne |
| Backupy | Automatyczne | Manualne |
| Multi-region | Tak | Nie |
Kiedy wybrać Turso?
- Edge applications - Dane blisko użytkowników
- Read-heavy workloads - Większość operacji to odczyty
- Low latency critical - Mikrosekundy mają znaczenie
- Simple schema - Relacyjne dane bez złożonych JOINów
- Cost-conscious - Tańsza alternatywa dla PostgreSQL
Instalacja i Konfiguracja
Turso CLI
# macOS / Linux
curl -sSfL https://get.tur.so/install.sh | bash
# macOS (Homebrew)
brew install tursodatabase/tap/turso
# Weryfikacja
turso --version
# Windows (WSL wymagany)
curl -sSfL https://get.tur.so/install.sh | bashLogowanie i tworzenie bazy
# Zaloguj się (otworzy przeglądarkę)
turso auth login
# Lub z tokenem
turso auth login --headless
# Stwórz nową bazę danych
turso db create my-database
# Z konkretną lokalizacją
turso db create my-database --location waw
# Lista baz
turso db list
# Szczegóły bazy
turso db show my-databaseDostępne lokalizacje (30+)
# Lista wszystkich lokalizacji
turso db locations
# Popularne lokalizacje:
# waw - Warsaw, Poland
# fra - Frankfurt, Germany
# lhr - London, UK
# ams - Amsterdam, Netherlands
# cdg - Paris, France
# iad - N. Virginia, USA
# sfo - San Francisco, USA
# sin - Singapore
# nrt - Tokyo, Japan
# syd - Sydney, Australia
# gru - São Paulo, BrazilTworzenie tokenu
# Token z pełnymi uprawnieniami
turso db tokens create my-database
# Token read-only
turso db tokens create my-database --read-only
# Token z expiration
turso db tokens create my-database --expiration 7d
# Revoke token
turso db tokens revoke my-database <token-name>Podstawowe Użycie
Turso CLI Shell
# Połącz się z bazą (interaktywny shell)
turso db shell my-database
# Wykonaj SQL
turso> CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
turso> INSERT INTO users (email, name) VALUES ('user@example.com', 'John');
turso> SELECT * FROM users;
# Wyjście
turso> .quitTypeScript/JavaScript Client
npm install @libsql/client// db/client.ts
import { createClient } from '@libsql/client'
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
// Lub dla lokalnego developmentu
// export const db = createClient({
// url: 'file:local.db'
// })// Podstawowe operacje
import { db } from './db/client'
// Query
const result = await db.execute('SELECT * FROM users')
console.log(result.rows)
// Z parametrami (named)
const user = await db.execute({
sql: 'SELECT * FROM users WHERE id = :id',
args: { id: 1 }
})
// Z parametrami (positional)
const users = await db.execute({
sql: 'SELECT * FROM users WHERE email = ?',
args: ['user@example.com']
})
// Insert
await db.execute({
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['new@example.com', 'Jane']
})
// Update
await db.execute({
sql: 'UPDATE users SET name = ? WHERE id = ?',
args: ['Updated Name', 1]
})
// Delete
await db.execute({
sql: 'DELETE FROM users WHERE id = ?',
args: [1]
})Transactions
// Pojedyncza transakcja
const result = await db.transaction(async (tx) => {
await tx.execute({
sql: 'INSERT INTO accounts (user_id, balance) VALUES (?, ?)',
args: [userId, 1000]
})
await tx.execute({
sql: 'INSERT INTO transactions (account_id, amount) VALUES (?, ?)',
args: [accountId, -100]
})
await tx.execute({
sql: 'UPDATE accounts SET balance = balance - ? WHERE id = ?',
args: [100, accountId]
})
return { success: true }
})Batch Queries
// Wykonaj wiele queries w jednym roundtrip
const results = await db.batch([
{
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['user1@example.com', 'User 1']
},
{
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['user2@example.com', 'User 2']
},
{
sql: 'SELECT * FROM users'
}
])
// results[0] - pierwszy INSERT
// results[1] - drugi INSERT
// results[2] - SELECTEdge Replicas
Dodawanie replik
# Dodaj replikę w konkretnej lokalizacji
turso db replicate my-database waw
turso db replicate my-database fra
turso db replicate my-database sin
# Lista replik
turso db show my-database
# Usuń replikę
turso db replicate my-database waw --removeAutomatyczne routing
// Klient automatycznie łączy się z najbliższą repliką
const db = createClient({
url: 'libsql://my-database-username.turso.io',
authToken: process.env.TURSO_AUTH_TOKEN
})
// Reads -> najbliższa replika
// Writes -> primary (propagowane do replik)Manual routing
// Połączenie do konkretnej lokalizacji
const db = createClient({
url: 'libsql://my-database-username.turso.io?location=waw',
authToken: process.env.TURSO_AUTH_TOKEN
})Embedded Replicas
Embedded replicas to game-changer - lokalna kopia bazy w aplikacji!
Konfiguracja
import { createClient } from '@libsql/client'
const db = createClient({
// Lokalna replika
url: 'file:local-replica.db',
// Synchronizacja z remote
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
// Opcjonalnie: automatyczna synchronizacja
syncInterval: 60 // sekundy
})Manual Sync
// Synchronizuj ręcznie
await db.sync()
// Po INSERT/UPDATE na remote
// Dane pojawią się lokalnie po sync
// Typowy flow:
// 1. Użytkownik czyta z embedded replica (~0.5ms)
// 2. Użytkownik zapisuje do remote
// 3. Aplikacja sync()
// 4. Nowe dane dostępne lokalnieUse Cases dla Embedded Replicas
// 1. Read-heavy aplikacje
// Większość odczytów z lokalnej repliki
const users = await db.execute('SELECT * FROM users LIMIT 100')
// ~0.5ms zamiast ~50ms!
// 2. Offline-first apps
// Dane dostępne bez internetu
const cachedData = await db.execute('SELECT * FROM cache')
// 3. Edge functions
// Lokalna replika w Vercel Edge Function
export const config = { runtime: 'edge' }
export default async function handler() {
const data = await db.execute('SELECT * FROM products')
return Response.json(data.rows)
}Embedded Replicas w Next.js
// lib/db.ts
import { createClient } from '@libsql/client'
// Singleton pattern dla embedded replica
let db: ReturnType<typeof createClient> | null = null
export function getDb() {
if (!db) {
db = createClient({
url: 'file:./local.db',
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN
})
}
return db
}
// Sync na starcie aplikacji
export async function initDb() {
const db = getDb()
await db.sync()
}// app/api/users/route.ts
import { getDb } from '@/lib/db'
import { NextResponse } from 'next/server'
export async function GET() {
const db = getDb()
// Super szybki odczyt z lokalnej repliki
const result = await db.execute('SELECT * FROM users')
return NextResponse.json(result.rows)
}
export async function POST(request: Request) {
const db = getDb()
const data = await request.json()
// Zapis idzie do remote
await db.execute({
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: [data.email, data.name]
})
// Sync dla natychmiastowej dostępności lokalnie
await db.sync()
return NextResponse.json({ success: true })
}Drizzle ORM
Setup
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').notNull().references(() => users.id),
published: integer('published', { mode: 'boolean' }).default(false),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})// db/index.ts
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 })// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './drizzle',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
}
} satisfies ConfigMigracje
# Generuj migracje
npx drizzle-kit generate:sqlite
# Aplikuj migracje
npx drizzle-kit push:sqlite
# Studio (GUI)
npx drizzle-kit studioQueries z Drizzle
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and, desc, like } from 'drizzle-orm'
// Select all
const allUsers = await db.select().from(users)
// Select with where
const user = await db
.select()
.from(users)
.where(eq(users.id, 1))
// Select specific columns
const emails = await db
.select({ email: users.email })
.from(users)
// Join
const postsWithAuthors = await db
.select({
post: posts,
author: users
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
// Insert
const [newUser] = await db
.insert(users)
.values({
email: 'new@example.com',
name: 'New User'
})
.returning()
// Update
await db
.update(users)
.set({ name: 'Updated' })
.where(eq(users.id, 1))
// Delete
await db
.delete(users)
.where(eq(users.id, 1))
// Search
const searchResults = await db
.select()
.from(users)
.where(like(users.name, '%john%'))Prisma
Setup
npm install prisma @prisma/client
npm install @prisma/adapter-libsql @libsql/client
npx prisma init// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaLibSQL } from '@prisma/adapter-libsql'
import { createClient } from '@libsql/client'
const libsql = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
const adapter = new PrismaLibSQL(libsql)
export const prisma = new PrismaClient({ adapter })# Push schema do Turso
npx prisma db push
# Generuj client
npx prisma generate// Użycie
import { prisma } from '@/lib/prisma'
// Create
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John'
}
})
// Read with relations
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
})
// Update
await prisma.user.update({
where: { id: 1 },
data: { name: 'Updated' }
})
// Delete
await prisma.post.delete({
where: { id: 1 }
})Database Groups
Groups pozwalają zarządzać wieloma bazami razem:
# Stwórz grupę
turso group create my-group --location waw
# Dodaj bazę do grupy
turso db create my-db --group my-group
# Wszystkie bazy w grupie dziedziczą lokalizacje
turso group locations add my-group fra
# Lista grup
turso group list
# Tokeny na poziomie grupy
turso group tokens create my-groupMulti-tenant Architecture
// Każdy tenant ma własną bazę w grupie
async function createTenantDb(tenantId: string) {
// Użyj Turso Platform API
const response = await fetch(
`https://api.turso.tech/v1/organizations/${orgId}/databases`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${TURSO_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: `tenant-${tenantId}`,
group: 'tenants-group'
})
}
)
return response.json()
}
// Routing do właściwej bazy
function getTenantDb(tenantId: string) {
return createClient({
url: `libsql://tenant-${tenantId}-${org}.turso.io`,
authToken: process.env.TURSO_AUTH_TOKEN
})
}Schema Management
Turso CLI Schema
# Pokaż schemat
turso db shell my-database ".schema"
# Export schema
turso db shell my-database ".schema" > schema.sql
# Import schema
turso db shell my-database < schema.sqlMigracje z Drizzle
# Struktura
migrations/
├── 0000_init.sql
├── 0001_add_posts.sql
└── 0002_add_comments.sql// migrate.ts
import { migrate } from 'drizzle-orm/libsql/migrator'
import { db } from './db'
async function main() {
console.log('Running migrations...')
await migrate(db, { migrationsFolder: './drizzle' })
console.log('Migrations complete!')
}
main()Schema Dump i Restore
# Dump całej bazy
turso db shell my-database ".dump" > backup.sql
# Restore
turso db create my-database-restored
turso db shell my-database-restored < backup.sqlIntegracje
Next.js App Router
// app/api/users/route.ts
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { NextResponse } from 'next/server'
export async function GET() {
const allUsers = await db.select().from(users)
return NextResponse.json(allUsers)
}
export async function POST(request: Request) {
const data = await request.json()
const [user] = await db
.insert(users)
.values(data)
.returning()
return NextResponse.json(user, { status: 201 })
}Vercel Edge Functions
// app/api/edge/route.ts
import { createClient } from '@libsql/client/web'
export const runtime = 'edge'
const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [id]
})
return Response.json(result.rows[0])
}SvelteKit
// src/lib/db.ts
import { createClient } from '@libsql/client'
import { TURSO_DATABASE_URL, TURSO_AUTH_TOKEN } from '$env/static/private'
export const db = createClient({
url: TURSO_DATABASE_URL,
authToken: TURSO_AUTH_TOKEN
})// src/routes/api/users/+server.ts
import { db } from '$lib/db'
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async () => {
const result = await db.execute('SELECT * FROM users')
return json(result.rows)
}Remix
// app/utils/db.server.ts
import { createClient } from '@libsql/client'
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})// app/routes/users.tsx
import { json } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { db } from '~/utils/db.server'
export async function loader() {
const result = await db.execute('SELECT * FROM users')
return json({ users: result.rows })
}
export default function Users() {
const { users } = useLoaderData<typeof loader>()
return (
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}GitHub Actions CI/CD
# .github/workflows/migrate.yml
name: Database Migration
on:
push:
branches: [main]
paths: ['drizzle/**']
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run migrations
run: npx drizzle-kit push:sqlite
env:
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}Performance Tips
Indeksy
-- Twórz indeksy dla często używanych kolumn
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
-- Composite index
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);Query Optimization
// ❌ Pobierz wszystko
const users = await db.execute('SELECT * FROM users')
// ✅ Pobierz tylko potrzebne kolumny
const users = await db.execute('SELECT id, name FROM users')
// ❌ Brak limitu
const posts = await db.execute('SELECT * FROM posts ORDER BY created_at DESC')
// ✅ Z limitem
const posts = await db.execute({
sql: 'SELECT * FROM posts ORDER BY created_at DESC LIMIT ?',
args: [20]
})
// ✅ Pagination
const posts = await db.execute({
sql: 'SELECT * FROM posts ORDER BY id LIMIT ? OFFSET ?',
args: [20, page * 20]
})Connection Reuse
// ❌ Nowy client przy każdym request
export async function handler() {
const db = createClient({ ... })
// ...
}
// ✅ Singleton pattern
let db: ReturnType<typeof createClient> | null = null
export function getDb() {
if (!db) {
db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
}
return db
}Batch vs Multiple Queries
// ❌ Wiele roundtrips
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
// ✅ Jeden roundtrip
await db.batch([
{ sql: 'INSERT INTO users ...', args: [...] },
{ sql: 'INSERT INTO users ...', args: [...] },
{ sql: 'INSERT INTO users ...', args: [...] }
])Cennik
Starter (Free)
- Storage: 9GB total
- Databases: 500
- Row reads: 1 miliard/miesiąc
- Row writes: 25 milionów/miesiąc
- Locations: 3 repliki
- Groups: 3
Scaler ($29/miesiąc)
- Storage: 24GB (+ $0.75/GB)
- Databases: Unlimited
- Row reads: Unlimited
- Row writes: 100 milionów (+ $1/milion)
- Locations: 6 replik
- Groups: Unlimited
- Point-in-time recovery
Enterprise (Custom)
- Dedicated support
- SLA
- Custom limits
- SSO/SAML
- Audit logs
Best Practices
Development Workflow
# 1. Lokalna baza do developmentu
turso db create my-app-dev
# 2. Staging
turso db create my-app-staging
# 3. Production
turso db create my-app-prod --location waw
turso db replicate my-app-prod fra
turso db replicate my-app-prod iadEnvironment Variables
# .env.local (development)
TURSO_DATABASE_URL=libsql://my-app-dev-username.turso.io
TURSO_AUTH_TOKEN=eyJhbGci...
# .env.production
TURSO_DATABASE_URL=libsql://my-app-prod-username.turso.io
TURSO_AUTH_TOKEN=eyJhbGci...Error Handling
import { db } from '@/lib/db'
async function getUser(id: number) {
try {
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [id]
})
if (result.rows.length === 0) {
return null
}
return result.rows[0]
} catch (error) {
if (error instanceof Error) {
console.error('Database error:', error.message)
// Retry logic dla network errors
if (error.message.includes('network')) {
// Retry...
}
}
throw error
}
}Type Safety
// types/db.ts
interface User {
id: number
email: string
name: string | null
createdAt: string
}
// Typowany query
async function getUsers(): Promise<User[]> {
const result = await db.execute('SELECT * FROM users')
return result.rows as User[]
}
// Lub z Drizzle (pełna type safety)
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
const allUsers = await db.select().from(users)
// allUsers jest automatycznie typowany!FAQ - Najczęściej Zadawane Pytania
Kiedy używać embedded replicas?
Gdy masz read-heavy workload i zależy Ci na ultra-niskich latencjach. Idealne dla e-commerce (produkty), CMS (artykuły), analytics dashboards.
Czy Turso wspiera foreign keys?
Tak! W przeciwieństwie do PlanetScale, Turso/libSQL w pełni wspiera SQLite foreign keys.
Jak działa synchronizacja embedded replicas?
Embedded replica synchronizuje się z primary przy wywołaniu db.sync(). Możesz też ustawić syncInterval dla automatycznej synchronizacji.
Czy mogę używać Turso z istniejącą bazą SQLite?
Tak! Możesz zaimportować istniejącą bazę SQLite do Turso przez CLI lub API.
Jaka jest różnica między Turso a D1 (Cloudflare)?
D1 działa tylko na Cloudflare Workers. Turso jest niezależny od platformy i oferuje embedded replicas oraz więcej edge locations.
Czy Turso wspiera full-text search?
Tak, przez SQLite FTS5 extension. libSQL wspiera wszystkie standardowe rozszerzenia SQLite.
-- FTS5 przykład
CREATE VIRTUAL TABLE posts_fts USING fts5(title, content);
INSERT INTO posts_fts SELECT title, content FROM posts;
SELECT * FROM posts_fts WHERE posts_fts MATCH 'search query';Podsumowanie
Turso to idealna baza danych dla nowoczesnych edge aplikacji:
- Edge locations - dane blisko użytkowników
- Embedded replicas - mikrosekundowe latencje
- libSQL - otwarty SQLite z supermocami
- Serverless - zero zarządzania infrastrukturą
- Cost-effective - znacznie tańszy niż alternatywy
Świetny wybór dla aplikacji wymagających niskich latencji, read-heavy workloads i prostej architektury bazodanowej.
Turso - a complete guide to edge SQLite database
What is Turso?
Turso is a distributed SQLite database designed for modern edge applications. It uses libSQL - an open fork of SQLite developed by the Turso team - which adds features like replication, HTTP/WebSocket access, and extensions while maintaining full SQLite compatibility.
The main innovation behind Turso is the ability to place database replicas close to users around the world (edge locations), which drastically reduces latencies. On top of that, Turso offers embedded replicas - a local copy of the database inside your application that syncs with the primary database, delivering microsecond read times.
Why Turso?
Key advantages of Turso
- Edge locations - Replicas around the world, close to users
- Embedded replicas - Local database copy inside your app
- libSQL - Open fork of SQLite with additional features
- Ultra-low latencies - Microseconds for local reads
- SQLite compatibility - Full compatibility with SQLite
- Serverless - Automatic scaling, zero management
- Cost-effective - Significantly cheaper than PostgreSQL/MySQL
- Simple - One file = entire database
Turso vs other databases
| Feature | Turso | PlanetScale | Neon | D1 (Cloudflare) |
|---|---|---|---|---|
| Engine | SQLite/libSQL | MySQL/Vitess | PostgreSQL | SQLite |
| Edge locations | 30+ | 10+ | 10+ | Cloudflare PoPs |
| Embedded replicas | Yes | No | No | No |
| Local latency | ~0.5ms | ~50ms | ~50ms | ~5ms |
| Free tier | 9GB | 5GB | 3GB | 5GB |
| Branching | Yes | Yes | Yes | No |
| Open source | libSQL (yes) | No | No | No |
Turso vs SQLite
| Feature | Turso | SQLite |
|---|---|---|
| Replication | Automatic | None |
| Edge deployment | Yes | No |
| HTTP access | Yes | No |
| Websockets | Yes | No |
| Scaling | Automatic | Manual |
| Backups | Automatic | Manual |
| Multi-region | Yes | No |
When to choose Turso?
- Edge applications - Data close to users
- Read-heavy workloads - Most operations are reads
- Low latency critical - Microseconds matter
- Simple schema - Relational data without complex JOINs
- Cost-conscious - Cheaper alternative to PostgreSQL
Installation and configuration
Turso CLI
# macOS / Linux
curl -sSfL https://get.tur.so/install.sh | bash
# macOS (Homebrew)
brew install tursodatabase/tap/turso
# Verify
turso --version
# Windows (WSL required)
curl -sSfL https://get.tur.so/install.sh | bashLogin and creating a database
# Log in (opens browser)
turso auth login
# Or with a token
turso auth login --headless
# Create a new database
turso db create my-database
# With a specific location
turso db create my-database --location waw
# List databases
turso db list
# Database details
turso db show my-databaseAvailable locations (30+)
# List all locations
turso db locations
# Popular locations:
# waw - Warsaw, Poland
# fra - Frankfurt, Germany
# lhr - London, UK
# ams - Amsterdam, Netherlands
# cdg - Paris, France
# iad - N. Virginia, USA
# sfo - San Francisco, USA
# sin - Singapore
# nrt - Tokyo, Japan
# syd - Sydney, Australia
# gru - São Paulo, BrazilCreating a token
# Token with full permissions
turso db tokens create my-database
# Read-only token
turso db tokens create my-database --read-only
# Token with expiration
turso db tokens create my-database --expiration 7d
# Revoke token
turso db tokens revoke my-database <token-name>Basic usage
Turso CLI Shell
# Connect to the database (interactive shell)
turso db shell my-database
# Execute SQL
turso> CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
turso> INSERT INTO users (email, name) VALUES ('user@example.com', 'John');
turso> SELECT * FROM users;
# Exit
turso> .quitTypeScript/JavaScript client
npm install @libsql/client// db/client.ts
import { createClient } from '@libsql/client'
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
// Or for local development
// export const db = createClient({
// url: 'file:local.db'
// })// Basic operations
import { db } from './db/client'
// Query
const result = await db.execute('SELECT * FROM users')
console.log(result.rows)
// With parameters (named)
const user = await db.execute({
sql: 'SELECT * FROM users WHERE id = :id',
args: { id: 1 }
})
// With parameters (positional)
const users = await db.execute({
sql: 'SELECT * FROM users WHERE email = ?',
args: ['user@example.com']
})
// Insert
await db.execute({
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['new@example.com', 'Jane']
})
// Update
await db.execute({
sql: 'UPDATE users SET name = ? WHERE id = ?',
args: ['Updated Name', 1]
})
// Delete
await db.execute({
sql: 'DELETE FROM users WHERE id = ?',
args: [1]
})Transactions
// Single transaction
const result = await db.transaction(async (tx) => {
await tx.execute({
sql: 'INSERT INTO accounts (user_id, balance) VALUES (?, ?)',
args: [userId, 1000]
})
await tx.execute({
sql: 'INSERT INTO transactions (account_id, amount) VALUES (?, ?)',
args: [accountId, -100]
})
await tx.execute({
sql: 'UPDATE accounts SET balance = balance - ? WHERE id = ?',
args: [100, accountId]
})
return { success: true }
})Batch queries
// Execute multiple queries in a single roundtrip
const results = await db.batch([
{
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['user1@example.com', 'User 1']
},
{
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['user2@example.com', 'User 2']
},
{
sql: 'SELECT * FROM users'
}
])
// results[0] - first INSERT
// results[1] - second INSERT
// results[2] - SELECTEdge replicas
Adding replicas
# Add a replica in a specific location
turso db replicate my-database waw
turso db replicate my-database fra
turso db replicate my-database sin
# List replicas
turso db show my-database
# Remove a replica
turso db replicate my-database waw --removeAutomatic routing
// The client automatically connects to the nearest replica
const db = createClient({
url: 'libsql://my-database-username.turso.io',
authToken: process.env.TURSO_AUTH_TOKEN
})
// Reads -> nearest replica
// Writes -> primary (propagated to replicas)Manual routing
// Connect to a specific location
const db = createClient({
url: 'libsql://my-database-username.turso.io?location=waw',
authToken: process.env.TURSO_AUTH_TOKEN
})Embedded replicas
Embedded replicas are a game-changer - a local copy of the database right inside your app!
Configuration
import { createClient } from '@libsql/client'
const db = createClient({
// Local replica
url: 'file:local-replica.db',
// Sync with remote
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
// Optional: automatic sync
syncInterval: 60 // seconds
})Manual sync
// Sync manually
await db.sync()
// After INSERT/UPDATE on remote
// Data will appear locally after sync
// Typical flow:
// 1. User reads from embedded replica (~0.5ms)
// 2. User writes to remote
// 3. App calls sync()
// 4. New data available locallyUse cases for embedded replicas
// 1. Read-heavy applications
// Most reads from the local replica
const users = await db.execute('SELECT * FROM users LIMIT 100')
// ~0.5ms instead of ~50ms!
// 2. Offline-first apps
// Data available without internet
const cachedData = await db.execute('SELECT * FROM cache')
// 3. Edge functions
// Local replica in a Vercel Edge Function
export const config = { runtime: 'edge' }
export default async function handler() {
const data = await db.execute('SELECT * FROM products')
return Response.json(data.rows)
}Embedded replicas in Next.js
// lib/db.ts
import { createClient } from '@libsql/client'
// Singleton pattern for the embedded replica
let db: ReturnType<typeof createClient> | null = null
export function getDb() {
if (!db) {
db = createClient({
url: 'file:./local.db',
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN
})
}
return db
}
// Sync on application startup
export async function initDb() {
const db = getDb()
await db.sync()
}// app/api/users/route.ts
import { getDb } from '@/lib/db'
import { NextResponse } from 'next/server'
export async function GET() {
const db = getDb()
// Super fast read from the local replica
const result = await db.execute('SELECT * FROM users')
return NextResponse.json(result.rows)
}
export async function POST(request: Request) {
const db = getDb()
const data = await request.json()
// Write goes to remote
await db.execute({
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: [data.email, data.name]
})
// Sync for immediate local availability
await db.sync()
return NextResponse.json({ success: true })
}Drizzle ORM
Setup
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').notNull().references(() => users.id),
published: integer('published', { mode: 'boolean' }).default(false),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})// db/index.ts
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 })// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './drizzle',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
}
} satisfies ConfigMigrations
# Generate migrations
npx drizzle-kit generate:sqlite
# Apply migrations
npx drizzle-kit push:sqlite
# Studio (GUI)
npx drizzle-kit studioQueries with Drizzle
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and, desc, like } from 'drizzle-orm'
// Select all
const allUsers = await db.select().from(users)
// Select with where
const user = await db
.select()
.from(users)
.where(eq(users.id, 1))
// Select specific columns
const emails = await db
.select({ email: users.email })
.from(users)
// Join
const postsWithAuthors = await db
.select({
post: posts,
author: users
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
// Insert
const [newUser] = await db
.insert(users)
.values({
email: 'new@example.com',
name: 'New User'
})
.returning()
// Update
await db
.update(users)
.set({ name: 'Updated' })
.where(eq(users.id, 1))
// Delete
await db
.delete(users)
.where(eq(users.id, 1))
// Search
const searchResults = await db
.select()
.from(users)
.where(like(users.name, '%john%'))Prisma
Setup
npm install prisma @prisma/client
npm install @prisma/adapter-libsql @libsql/client
npx prisma init// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaLibSQL } from '@prisma/adapter-libsql'
import { createClient } from '@libsql/client'
const libsql = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
const adapter = new PrismaLibSQL(libsql)
export const prisma = new PrismaClient({ adapter })# Push schema to Turso
npx prisma db push
# Generate client
npx prisma generate// Usage
import { prisma } from '@/lib/prisma'
// Create
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John'
}
})
// Read with relations
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
})
// Update
await prisma.user.update({
where: { id: 1 },
data: { name: 'Updated' }
})
// Delete
await prisma.post.delete({
where: { id: 1 }
})Database groups
Groups let you manage multiple databases together:
# Create a group
turso group create my-group --location waw
# Add a database to the group
turso db create my-db --group my-group
# All databases in the group inherit locations
turso group locations add my-group fra
# List groups
turso group list
# Group-level tokens
turso group tokens create my-groupMulti-tenant architecture
// Each tenant has its own database in the group
async function createTenantDb(tenantId: string) {
// Use the Turso Platform API
const response = await fetch(
`https://api.turso.tech/v1/organizations/${orgId}/databases`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${TURSO_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: `tenant-${tenantId}`,
group: 'tenants-group'
})
}
)
return response.json()
}
// Route to the correct database
function getTenantDb(tenantId: string) {
return createClient({
url: `libsql://tenant-${tenantId}-${org}.turso.io`,
authToken: process.env.TURSO_AUTH_TOKEN
})
}Schema management
Turso CLI schema
# Show schema
turso db shell my-database ".schema"
# Export schema
turso db shell my-database ".schema" > schema.sql
# Import schema
turso db shell my-database < schema.sqlMigrations with Drizzle
# Structure
migrations/
├── 0000_init.sql
├── 0001_add_posts.sql
└── 0002_add_comments.sql// migrate.ts
import { migrate } from 'drizzle-orm/libsql/migrator'
import { db } from './db'
async function main() {
console.log('Running migrations...')
await migrate(db, { migrationsFolder: './drizzle' })
console.log('Migrations complete!')
}
main()Schema dump and restore
# Dump the entire database
turso db shell my-database ".dump" > backup.sql
# Restore
turso db create my-database-restored
turso db shell my-database-restored < backup.sqlIntegrations
Next.js App Router
// app/api/users/route.ts
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { NextResponse } from 'next/server'
export async function GET() {
const allUsers = await db.select().from(users)
return NextResponse.json(allUsers)
}
export async function POST(request: Request) {
const data = await request.json()
const [user] = await db
.insert(users)
.values(data)
.returning()
return NextResponse.json(user, { status: 201 })
}Vercel Edge Functions
// app/api/edge/route.ts
import { createClient } from '@libsql/client/web'
export const runtime = 'edge'
const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [id]
})
return Response.json(result.rows[0])
}SvelteKit
// src/lib/db.ts
import { createClient } from '@libsql/client'
import { TURSO_DATABASE_URL, TURSO_AUTH_TOKEN } from '$env/static/private'
export const db = createClient({
url: TURSO_DATABASE_URL,
authToken: TURSO_AUTH_TOKEN
})// src/routes/api/users/+server.ts
import { db } from '$lib/db'
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async () => {
const result = await db.execute('SELECT * FROM users')
return json(result.rows)
}Remix
// app/utils/db.server.ts
import { createClient } from '@libsql/client'
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})// app/routes/users.tsx
import { json } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { db } from '~/utils/db.server'
export async function loader() {
const result = await db.execute('SELECT * FROM users')
return json({ users: result.rows })
}
export default function Users() {
const { users } = useLoaderData<typeof loader>()
return (
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}GitHub Actions CI/CD
# .github/workflows/migrate.yml
name: Database Migration
on:
push:
branches: [main]
paths: ['drizzle/**']
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run migrations
run: npx drizzle-kit push:sqlite
env:
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}Performance tips
Indexes
-- Create indexes for frequently queried columns
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
-- Composite index
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);Query optimization
// ❌ Fetch everything
const users = await db.execute('SELECT * FROM users')
// ✅ Fetch only the columns you need
const users = await db.execute('SELECT id, name FROM users')
// ❌ No limit
const posts = await db.execute('SELECT * FROM posts ORDER BY created_at DESC')
// ✅ With a limit
const posts = await db.execute({
sql: 'SELECT * FROM posts ORDER BY created_at DESC LIMIT ?',
args: [20]
})
// ✅ Pagination
const posts = await db.execute({
sql: 'SELECT * FROM posts ORDER BY id LIMIT ? OFFSET ?',
args: [20, page * 20]
})Connection reuse
// ❌ New client on every request
export async function handler() {
const db = createClient({ ... })
// ...
}
// ✅ Singleton pattern
let db: ReturnType<typeof createClient> | null = null
export function getDb() {
if (!db) {
db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
}
return db
}Batch vs multiple queries
// ❌ Multiple roundtrips
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
// ✅ Single roundtrip
await db.batch([
{ sql: 'INSERT INTO users ...', args: [...] },
{ sql: 'INSERT INTO users ...', args: [...] },
{ sql: 'INSERT INTO users ...', args: [...] }
])Pricing
Starter (Free)
- Storage: 9GB total
- Databases: 500
- Row reads: 1 billion/month
- Row writes: 25 million/month
- Locations: 3 replicas
- Groups: 3
Scaler ($29/month)
- Storage: 24GB (+ $0.75/GB)
- Databases: Unlimited
- Row reads: Unlimited
- Row writes: 100 million (+ $1/million)
- Locations: 6 replicas
- Groups: Unlimited
- Point-in-time recovery
Enterprise (Custom)
- Dedicated support
- SLA
- Custom limits
- SSO/SAML
- Audit logs
Best practices
Development workflow
# 1. Local database for development
turso db create my-app-dev
# 2. Staging
turso db create my-app-staging
# 3. Production
turso db create my-app-prod --location waw
turso db replicate my-app-prod fra
turso db replicate my-app-prod iadEnvironment variables
# .env.local (development)
TURSO_DATABASE_URL=libsql://my-app-dev-username.turso.io
TURSO_AUTH_TOKEN=eyJhbGci...
# .env.production
TURSO_DATABASE_URL=libsql://my-app-prod-username.turso.io
TURSO_AUTH_TOKEN=eyJhbGci...Error handling
import { db } from '@/lib/db'
async function getUser(id: number) {
try {
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [id]
})
if (result.rows.length === 0) {
return null
}
return result.rows[0]
} catch (error) {
if (error instanceof Error) {
console.error('Database error:', error.message)
// Retry logic for network errors
if (error.message.includes('network')) {
// Retry...
}
}
throw error
}
}Type safety
// types/db.ts
interface User {
id: number
email: string
name: string | null
createdAt: string
}
// Typed query
async function getUsers(): Promise<User[]> {
const result = await db.execute('SELECT * FROM users')
return result.rows as User[]
}
// Or with Drizzle (full type safety)
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
const allUsers = await db.select().from(users)
// allUsers is automatically typed!FAQ - frequently asked questions
When to use embedded replicas?
When you have a read-heavy workload and ultra-low latencies matter to you. Ideal for e-commerce (products), CMS (articles), and analytics dashboards.
Does Turso support foreign keys?
Yes! Unlike PlanetScale, Turso/libSQL fully supports SQLite foreign keys.
How does embedded replica synchronization work?
The embedded replica syncs with the primary when you call db.sync(). You can also set syncInterval for automatic synchronization.
Can I use Turso with an existing SQLite database?
Yes! You can import an existing SQLite database into Turso via the CLI or API.
What is the difference between Turso and D1 (Cloudflare)?
D1 only works on Cloudflare Workers. Turso is platform-independent and offers embedded replicas along with more edge locations.
Does Turso support full-text search?
Yes, through the SQLite FTS5 extension. libSQL supports all standard SQLite extensions.
-- FTS5 example
CREATE VIRTUAL TABLE posts_fts USING fts5(title, content);
INSERT INTO posts_fts SELECT title, content FROM posts;
SELECT * FROM posts_fts WHERE posts_fts MATCH 'search query';Summary
Turso is an ideal database for modern edge applications:
- Edge locations - data close to users
- Embedded replicas - microsecond latencies
- libSQL - open SQLite with superpowers
- Serverless - zero infrastructure management
- Cost-effective - significantly cheaper than alternatives
A great choice for applications that require low latencies, read-heavy workloads, and simple database architecture.