frontback / databases / libsql

The libsql database

One SQLite-compatible database per project, spoken over HTTP and branchable like code. What it is good at, where it is the wrong tool, and what it costs.

What you get

A database is a project-level resource, not a hidden extra on a service. You create one, you link it to the services that should reach it, and you name the link. That name becomes the prefix of the environment variables the service receives, so a link called APP_DB arrives as APP_DB_URL and APP_DB_AUTH_TOKEN.

Nothing is injected behind your back. There is no implicit database and no magic DATABASE_URL, because a variable you did not ask for is a variable you cannot reason about.

main.ts
const url = Deno.env.get("APP_DB_URL")!;
const token = Deno.env.get("APP_DB_AUTH_TOKEN")!;

// libsql speaks HTTP: one request, one round trip, no pool.
async function query(sql: string) {
  const response = await fetch(`${url}/v2/pipeline`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      requests: [{ type: "execute", stmt: { sql } }, { type: "close" }],
    }),
  });
  return await response.json();
}

export default {
  async fetch() {
    const result = await query("select count(*) as n from visits");
    return Response.json(result);
  },
};

Any libsql or Turso client works too. The raw form is here because it shows the whole protocol: a POST with your statement, an answer, and no long-lived connection in between.

Branch it like a draft

A database can be forked. The copy starts as an exact snapshot of the original and then goes its own way, which is the same trick your editor does with a git branch, applied to data.

That is what makes staging useful here. Your staging environment can run against a fork of production data, so a migration is rehearsed against the real shape of the rows instead of an empty schema, and the original never notices. Promote when it works, throw the branch away when it does not.

Where it fits, and where it does not

  • No connection pool to size, exhaust or keep alive. A service that sleeps between requests pays nothing for an idle connection, which is exactly the shape a scale-to-zero platform wants.
  • The dialect is SQLite, so your queries, your migrations and most of your tooling already work, and so does running the same schema locally in a file.
  • Isolation is cryptographic, not conventional: every database is its own namespace with its own signing key, so one project's token cannot open another project's data.
  • Forking is cheap enough to be a habit, which turns 'test the migration' from a plan into a click.
  • It is SQLite: one writer at a time. Read-heavy and moderate-write workloads are comfortable, a write-saturated queue is not.
  • There is no Postgres extension ecosystem here. If your schema leans on PostGIS or pgvector, take the managed Postgres instead; it is one link away in the same project.
  • Sizes are capped per plan rather than elastic, so a database that grows without bound needs a plan that expects it.

What it costs

Databases are included in the plan rather than metered separately, and the allowance is shared between engines. Manual branches and per-release data states start on Pro.

  • Free, €010 databases, 50 MB each
  • Pro, €19 a month10 databases, 5 GB each
  • Business, €99 a month50 databases, 20 GB each

Examples that use it

Start from a working product

All examples

Every template deploys into your organisation as a real project, live at its own URL. Then it is yours: open the editor and change anything, or tell the AI what you want instead.

Email login with better-auth
Sign-up, sign-in and sessions with better-auth on a libsql database.
URL shortener
POST a long URL, get a short code, and 302-redirect from it.
Guestbook
A sign-my-guestbook form backed by libsql, with every entry safely escaped.
Uptime monitor
Ping a URL on a schedule and keep a browsable status history.
RSS digest
Collect new items from any RSS feed on a daily schedule.
Exchange rate logger
A scheduled script with no web endpoint: fetch, store, done.

Questions people ask about it

It is libsql, the fork of SQLite that added a server. The dialect and the file format are SQLite's; what is different is that you talk to it over HTTP instead of opening a file, which is what makes it usable from a service that sleeps between requests.

No, and that is the point. Each query is an HTTP request against your database's URL, authenticated with its token. Nothing is kept open between requests, so nothing leaks when a service scales to zero.

You link the database to the service and name the link. The name becomes the environment variable prefix: <code>APP_DB</code> gives you <code>APP_DB_URL</code> and <code>APP_DB_AUTH_TOKEN</code>. Unlink it and the variables are gone on the next deploy.

Yes. Fork the database and point the staging environment at the fork. Releases can keep their own data state too, so what a version served is still there when you roll back to it.

On our own machines in the EU, on the same node as your service where possible so a query is a local round trip. Snapshots go to object storage as the durability floor.

Create one. Postgres and Valkey are managed the same way, they link to a service the same way, and nothing stops a project from using all three. Your plan's database allowance is shared between them.

The other two

Go beyond what seems possible.