darkterminal/turso-client-http
A lightweight HTTP client for Turso/libSQL built for PHP and Laravel. Query remote Turso databases over HTTP without native extensions, using a simple, expressive API for raw SQL, migrations, and everyday database operations.
darkterminal/turso-client-http is a standalone, framework-agnostic PHP client for Turso (libSQL) that speaks the HTTP v2 pipeline API. It is not an Eloquent driver and not a PDO adapter — treat it as a thin HTTP gateway to a remote SQLite-compatible database.
Install and configure:
composer require darkterminal/turso-client-http
Add to .env (never commit the token):
TURSO_DATABASE_URL=libsql://your-db-yourorg.turso.io
TURSO_AUTH_TOKEN=eyJhbGciOi...
Where to look first: the README on GitHub. The public surface has changed between versions — the class is Darkterminal\TursoHttp\TursoClient (with a fluent DB-style builder in recent versions). Skim the README examples before writing a wrapper, so you match your installed version's method signatures.
First use case — smoke test from an Artisan command:
use Darkterminal\TursoHttp\TursoClient;
$client = new TursoClient(
url: config('services.turso.url'),
token: config('services.turso.token'),
);
$result = $client->query('SELECT name FROM sqlite_master WHERE type = ?', ['table']);
dump($result->toArray());
The natural first integration is read-only reference data (feature flags, country lists, lookup tables) pulled from Turso at boot or cached in Redis — high-value, low-risk, and doesn't require you to fight the fact that this is not a Laravel database connection.
Singleton via a service provider. One client instance per process; the underlying HTTP client connection is reused.
// config/services.php
'turso' => [
'url' => env('TURSO_DATABASE_URL'),
'token' => env('TURSO_AUTH_TOKEN'),
],
// AppServiceProvider::register()
$this->app->singleton(TursoClient::class, fn () => new TursoClient(
url: config('services.turso.url'),
token: config('services.turso.token'),
));
Repository layer. Do not let raw Turso calls leak into controllers. Wrap the client in a repository that returns domain objects, so you can later swap it for a local SQLite or read-cache implementation.
final class FeatureFlagRepository
{
public function __construct(private TursoClient $client) {}
public function enabled(string $key): bool
{
$row = $this->client->query(
'SELECT enabled FROM feature_flags WHERE key = ? LIMIT 1',
[$key]
)->first();
return (bool) ($row['enabled'] ?? false);
}
}
Batch instead of looping. Because every call is an HTTP round trip, collapse N queries into one pipeline call. This is the single biggest performance lever:
// Bad: N round trips
foreach ($ids as $id) {
$client->query('SELECT * FROM posts WHERE id = ?', [$id]);
}
// Good: one HTTP request
$client->batch(array_map(
fn ($id) => ['SELECT * FROM posts WHERE id = ?', [$id]],
$ids
));
Transactions for multi-write consistency. Wrap BEGIN/COMMIT statements in a single pipeline so they execute atomically on the edge:
$client->batch([
['BEGIN', []],
['INSERT INTO accounts (id, balance) VALUES (?, ?)', [$from, -100]],
['INSERT INTO accounts (id, balance) VALUES (?, ?)', [$to, 100]],
['COMMIT', []],
]);
Schema migrations. Turso is libSQL, so CREATE TABLE/ALTER TABLE works, but this package is not a migration runner. Recommended pattern: manage schema with Laravel's SQLite connection locally (DB::connection('sqlite')->statement(...)), then replay the same DDL through TursoClient::execute() in a dedicated Artisan command, or use the turso CLI in CI.
Cache the read path. Layer Laravel's cache in front of every read; remote HTTP reads are tens of milliseconds — fine for admin dashboards, too slow for hot paths without caching.
Cache::remember("flag:$key", now()->addMinutes(5), fn () => $this->repo->enabled($key));
Queued writes. Any non-trivial write should go through a job so a network hiccup on Turso doesn't fail the user's request.
{"type": "text"|"integer"|"float"|"blob"|"null", "value": ...}. The package usually unwraps these, but verify — if you get ["type" => "integer", "value" => "42"] back, you're reading raw pipeline output. Normalize at the repository boundary.error key inside batch() responses.allow_redirects explicitly or point at the primary URL for write traffic.libsql:// (the client upgrades TLS); pasting the raw https://.../v2/pipeline endpoint from the dashboard can bypass the client's own path handling. Copy the URL from turso db show <name> --url.exp. Rotate before expiry, and store per-environment in your secret manager, not .env in staging.413 or silent truncation usually means you exceeded this.RETURNING in older libSQL builds, no native BOOLEAN (0/1 integers), no DATETIME type (TEXT in ISO-8601). Cast in your repository, not in views.Blueprint, Schema, Eloquent relations — none of it works. If you need an ORM, look at the official libsql PHP extension or migrate to a Turso+Eloquent companion package; use this library for bounded read paths.Log::debug('turso', ['body' => $payload])) behind a TURSO_DEBUG flag. It's the fastest way to distinguish auth failures, schema errors, and type mismatches.ClientInterface; pass a pre-configured Guzzle client to add tracing (webhook/otel middleware), retries with backoff, or request timeouts (connect_timeout: 2, timeout: 5).composer.json ("darkterminal/turso-client-http": "^x.y") and add a thin abstraction so a future swap to the official SDK is a one-file change.How can I help you explore Laravel packages today?