libSQL is an open-source fork of SQLite that adds, among other things, a network server. Turso is the hosted service built on it. Together they make SQLite usable in places it never used to fit: a database per tenant, a database at the edge, a small app's whole backend. What they do not make obvious is how to point a desktop SQL client at one, because a libSQL database is neither a local file nor a traditional server with a username and password.
This post explains what you need to connect a GUI to Turso or a self-hosted libSQL server, what the auth token is, how the SQLite dialect behaves over the network, and where a desktop client fits alongside features like embedded replicas.
How clients talk to libSQL
A libSQL server (the self-hosted one is called sqld) speaks a protocol
called Hrana, and one of its transports is plain HTTP: the client posts a JSON
pipeline of statements and gets JSON results back. That design choice shapes
everything about connecting a GUI:
- There is no username. Access is a bearer token sent with each request, or nothing at all on a local server that has auth turned off.
- The address is a URL. Turso databases live at
libsql://<database>-<org>.turso.io, served over HTTPS on port 443. A self-hostedsqldlistens on port 8080 by default. - Nothing stays open between requests. Each script is its own exchange, which matters for transactions (more below).
Getting a URL and a token from Turso
With the Turso CLI installed and logged in, two commands give you everything a client needs:
# the database URL
turso db show shop --url
# libsql://shop-acme.turso.io
# a token for that database
turso db tokens create shop
# safer for browsing: read-only, and it expires
turso db tokens create shop --read-only --expiration 7d
Treat the token like a password, because it is one. For a GUI you mostly use to
look at data, the --read-only flag is worth the extra few characters:
the server then refuses writes whatever the client sends. An expiration keeps a
forgotten token from living forever.
Running libSQL locally
For development you do not need Turso at all. The libSQL server runs in Docker:
docker run -d --name libsql -p 8080:8080 ghcr.io/tursodatabase/libsql-server:latest
That gives you http://127.0.0.1:8080 with no token required, which is
the quickest way to try the rest of this post.
Connecting SQLly to libSQL or Turso
SQLly has a native libSQL driver that speaks the Hrana HTTP pipeline directly, so there is no SDK or bridge process involved. In the connection editor, choose libSQL and fill in two things: the host and the Auth token field. Leave the token blank for an anonymous local server.
You can also paste a URL and let the editor fill itself in. SQLly understands all three shapes you are likely to have lying around:
libsql://shop-acme.turso.io?authToken=eyJhbGciOi...
https://shop-acme.turso.io
http://127.0.0.1:8080
libsql:// and https:// mean TLS on port 443;
http:// means a plaintext local server. An authToken in the
URL is moved into the token field rather than kept in the address. A few details
worth knowing about how the token is handled:
- It is stored in your operating system's keychain, like a password, not in the connection file.
- Any
*.turso.iohost is always contacted over HTTPS, so a token cannot accidentally travel in the clear because of a mistyped scheme. - Exported connection definitions carry no token.
Self-hosted servers that are not reachable directly work through the same SSH tunnels, proxies and Kubernetes port-forwards as every other network connection in SQLly.
It is SQLite, over the network
Once connected, you are writing SQLite's dialect, so everything you know about
SQLite applies: INTEGER PRIMARY KEY rowid aliases, flexible typing,
strftime for dates, json_extract for JSON. Explore the schema
the SQLite way:
SELECT name, type, sql
FROM sqlite_schema
WHERE type IN ('table', 'view', 'index')
ORDER BY type, name;
SELECT * FROM pragma_table_info('orders');
You rarely need to, though. SQLly runs its SQLite catalog queries against libSQL,
so the explorer tree and IntelliSense
work as they do for a local SQLite file. Query plans
come from EXPLAIN QUERY PLAN and appear in SQLly's
plan views.
Transactions: keep them in one run
Because nothing is held open between requests, SQLly sends a whole script as one request: it splits the statements locally, then runs them as a single batch on one server-side stream, where each step only runs if the previous one succeeded. Two consequences follow.
First, a transaction works when BEGIN and COMMIT run
together:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
If the second UPDATE fails, the script stops there with the server's
own error message, and the transaction is rolled back. Second, and this is the one
that catches people: do not run BEGIN on its own, then the next
statement a minute later, and expect them to share a transaction. Select the whole
block and run it together.
The same mechanism is what makes SQLly's
rollback wrap work on libSQL: with
it on, your script runs inside BEGIN; ... ROLLBACK; in one request, so
you can see what an UPDATE would do without keeping it. And the
read-only connection setting refuses
anything that is not a read before it is sent, which pairs nicely with a read-only
Turso token.
Large results: page them
Hrana returns each result in one response rather than streaming it row by row. SQLly reads the response and feeds it to the grid in batches, so the grid still paints progressively, but a very large result costs its full size in memory before the first row appears. Page instead of pulling everything:
SELECT id, customer_id, total, created_at
FROM orders
WHERE id > 25000 -- the last id from the previous page
ORDER BY id
LIMIT 1000;
Keyset paging like this (remember the last id you saw) stays fast on
large tables, where OFFSET gets slower the deeper you go. The related
caveat is cancellation: pressing Cancel drops the in-flight request, but libSQL has
no way to kill a running statement, so a long query finishes on the server anyway.
Put a LIMIT on exploratory queries.
What about embedded replicas?
Embedded replicas are a feature of the libSQL client SDKs: your application keeps a local SQLite file that syncs from the remote primary, so reads are local and fast. They are designed for applications, not for a SQL client. SQLly connects to the remote database over HTTP and does not create or sync an embedded replica. For looking at the data, the remote database is the source of truth anyway, and it is what you want to be querying.
Which tools work on libSQL
SQLly reuses its SQLite tooling for libSQL wherever the server allows it:
Disk Usage,
Schema Compare and
Data Compare, the
test data generator,
the structure editor, and dumps. Maintenance is a smaller set than local SQLite,
REINDEX and an integrity check, because the server refuses
VACUUM, ANALYZE and PRAGMA optimize. Editing
rows directly in the grid, Data Transfer into libSQL, Activity Monitor, Index
Analyzer, Security and Backup are not available for libSQL, and each says why rather
than showing an empty pane.
If you have a Turso database and have been poking at it through the web shell,
download SQLly, create a read-only token, paste the
libsql:// URL into a new connection, and you should be browsing tables a
minute later.
Keep reading
- Azure Data Studio is retired: where SQL Server users go next
- Query Parquet and CSV files with DuckDB in a desktop GUI
- Reading query plans in PostgreSQL, SQL Server, MySQL, SQLite
- Working with ClickHouse from a desktop SQL client
- Catch UPDATE/DELETE without WHERE before it runs
- Streaming huge result sets without eating your RAM
- Keep pivots, charts and formatting in your SQL
- IntelliSense that writes JOIN ON from foreign keys