ActiveRecord adapter for Turso (libSQL) database.
Connects Rails / ActiveRecord models to Turso Cloud via the Hrana v2 HTTP protocol,
implemented in pure Ruby using Net::HTTP — no native extension, no Rust toolchain required.
- Ruby >= 3.1
- ActiveRecord >= 7.0
# Gemfile
gem "activerecord-libsql"bundle installNo compile step needed. The gem is pure Ruby.
default: &default
adapter: turso
database: <%= ENV["TURSO_DATABASE_URL"] %> # libsql://xxx.turso.io
token: <%= ENV["TURSO_AUTH_TOKEN"] %>
development:
<<: *default
production:
<<: *defaultNote: Use the
database:key, noturl:. ActiveRecord tries to resolve the adapter from the URL scheme whenurl:is used, which causes a lookup failure.
require "activerecord-libsql"
ActiveRecord::Base.establish_connection(
adapter: "turso",
database: "libsql://your-db.turso.io",
token: "your-auth-token"
)class User < ActiveRecord::Base
end
# Create
User.create!(name: "Alice", email: "alice@example.com")
# Read
User.where(name: "Alice").first
User.find(1)
User.order(:name).limit(10)
# Update
User.find(1).update!(email: "new@example.com")
# Delete
User.find(1).destroyEmbedded Replicas keep a local SQLite copy of your Turso database on disk, synced from the remote. Reads are served locally (sub-millisecond), writes go to the remote.
# database.yml
production:
adapter: turso
database: <%= ENV["TURSO_DATABASE_URL"] %> # libsql://xxx.turso.io
token: <%= ENV["TURSO_AUTH_TOKEN"] %>
replica_path: /var/data/myapp.db # local replica file path
sync_interval: 60 # background sync every 60 seconds (0 = manual only)Or via establish_connection:
ActiveRecord::Base.establish_connection(
adapter: "turso",
database: "libsql://your-db.turso.io",
token: "your-auth-token",
replica_path: "/var/data/myapp.db",
sync_interval: 60
)# Trigger a sync from the remote at any time
ActiveRecord::Base.connection.syncreplica_pathmust point to a writable path. The file is created automatically on first connect.sync_intervalis in seconds. Set to0or omit to use manual sync only.- Multi-process (Puma / Solid Queue): Each worker process gets its own SQLite connection.
sqlite3gem 2.x handles fork safety automatically — connections are closed afterfork()and reopened in the child process. Do not share the samereplica_pathacross multiple Puma workers; use a unique path per worker (e.g./var/data/myapp-worker-#{worker_id}.db).
This adapter is compatible with Solid Queue.
FOR UPDATE SKIP LOCKED: Solid Queue uses this clause by default. libSQL and SQLite do not support it, so the adapter strips it automatically before sending SQL to the backend. SQLite serializes all writes, so row-level locking is not needed.- Fork safety: Solid Queue forks worker processes. The adapter handles this correctly —
sqlite3gem 2.x closes connections afterfork(), and ActiveRecord'sdiscard!/reconnectflow re-establishes them in the child process.
# config/queue.yml
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 3
polling_interval: 0.1turso:schema:apply and turso:schema:diff use sqldef
(sqlite3def) to manage your Turso schema declaratively — no migration files, no version
tracking. You define the desired schema in a .sql file and the task computes and applies
only the diff.
# macOS
brew install sqldef/sqldef/sqlite3def
# Other platforms: https://github.com/sqldef/sqldef/releasesreplica_path must be configured in database.yml (the tasks use the local replica to
compute the diff without touching the remote directly).
Applies the diff between your desired schema and the current remote schema.
rake turso:schema:apply[db/schema.sql]Shows what would be applied without making any changes (dry-run).
rake turso:schema:diff[db/schema.sql]Plain SQL CREATE TABLE statements. sqldef handles ALTER TABLE / CREATE INDEX / DROP
automatically based on the diff.
CREATE TABLE users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
);Rails Model (ActiveRecord)
↓ Arel → SQL string
LibsqlAdapter (lib/active_record/connection_adapters/libsql_adapter.rb)
↓ perform_query (strips FOR UPDATE, expands bind params)
↓
├─ Remote mode ──→ TursoLibsql::Connection (Hrana v2 HTTP via Net::HTTP)
│ ↓
│ Turso Cloud (HTTPS)
│
└─ Embedded Replica / Offline mode
↓
TursoLibsql::LocalConnection (sqlite3 gem)
↓
Local SQLite file ←─ sync ─→ Turso Cloud
The original implementation used a Rust native extension (tokio + rustls). On macOS,
all Rust TLS libraries are unsafe after fork() — they cause SEGV or deadlocks in
multi-process servers (Puma, Unicorn, Solid Queue). Ruby's Net::HTTP has no such
restriction and is fully fork-safe.
ActiveRecord's ConnectionPool issues a separate Adapter instance per thread, so
@raw_connection is never shared across threads. Net::HTTP opens a new TCP connection
per request, which is safe for concurrent use.
Benchmarked against a Turso cloud database (remote, over HTTPS) from a MacBook on a home network. All numbers include full round-trip network latency.
| Operation | ops/sec | avg latency |
|---|---|---|
| INSERT single row | 9.9 | 101.5 ms |
| SELECT all (100 rows) | 29.1 | 34.3 ms |
| SELECT WHERE | 35.9 | 27.9 ms |
| SELECT find by id | 16.2 | 61.9 ms |
| UPDATE single row | 6.4 | 156.0 ms |
| DELETE single row | 6.9 | 145.2 ms |
| Transaction (10 inserts) | 1.9 | 539.0 ms |
Environment: Ruby 3.4.8 · ActiveRecord 8.1.2 · Turso cloud (remote) · macOS arm64 Run
bundle exec ruby bench/benchmark.rbto reproduce.
Latency is dominated by network round-trips to the Turso cloud endpoint. For lower latency, use Embedded Replicas — reads are served from a local SQLite file with sub-millisecond latency.
| Feature | Status |
|---|---|
| SELECT / INSERT / UPDATE / DELETE | ✅ |
| Transactions | ✅ |
| Migrations (basic DDL) | ✅ |
| Schema management (sqldef) | ✅ |
| Bind parameters | ✅ |
| NOT NULL / UNIQUE / FK constraint → AR exceptions | ✅ |
| Embedded Replica (local reads) | ✅ |
| Offline write mode | ✅ |
| Solid Queue compatibility | ✅ |
| Fork safety (Puma / Solid Queue) | ✅ |
| Prepared statements (server-side) | ❌ libSQL HTTP does not support them |
| EXPLAIN | ❌ |
| Savepoints | ❌ |
# Unit tests only (no credentials needed)
bundle exec rake spec
# Integration tests (requires TURSO_DATABASE_URL and TURSO_AUTH_TOKEN)
bundle exec rake spec:integration
# All tests
bundle exec rake spec:allSet SKIP_INTEGRATION_TESTS=1 to skip integration tests in CI environments without Turso
credentials.
MIT