How to Use Turso with Astro: Complete Guide
Step-by-step guide to integrating Turso with your Astro website.
Turso is an edge-native database built on libSQL, which is an open-source fork of SQLite. Instead of running a centralized database server, Turso replicates your data to edge locations close to your users. The result is read latencies measured in single-digit milliseconds, no matter where your visitors are.
For Astro projects that need a database but want to keep things lightweight, Turso is a compelling option. You get the simplicity of SQLite with the scalability of a distributed database. No connection pooling headaches, no complex cluster management.
Prerequisites
- Node.js 22.12.0 or newer. Astro 6 sets this floor in its
enginesfield, so older Node versions will fail to install or build. - An Astro project (
npm create astro@latest). This guide was checked against Astro 6.4.2. - A Turso account. The current Free plan gives you 100 databases, 5GB of storage, 500 million row reads per month, and 10 million row writes per month.
- The Turso CLI installed. On macOS use
brew install tursodatabase/tap/turso. On Linux (or macOS without Homebrew) use the install script:
curl -sSfL https://get.tur.so/install.sh | bash
After installing, open a new shell and run turso to confirm the binary is on your PATH.
Installation
Install the Turso client library. Pin the current version so your lockfile is reproducible:
npm install @libsql/client@0.17.3
The package ships several entry points (., ./web, ./node, ./http, ./ws, ./sqlite3). Both the official Turso guide and the Astro docs import from @libsql/client/web, which is the variant designed for serverless and edge runtimes and the one used throughout this guide.
Create a database using the Turso CLI:
turso db create my-astro-blog
turso db show my-astro-blog --url
turso db tokens create my-astro-blog
Save the URL and token. You will need them in the next step.
Configuration
Add your Turso credentials to .env:
TURSO_DATABASE_URL=libsql://your-database-name-your-org.turso.io
TURSO_AUTH_TOKEN=your_auth_token_here
A note on the variable names. Do not prefix these with PUBLIC_. The Astro docs warn explicitly that the PUBLIC_ prefix exposes a value to the client bundle, which would leak your auth token to every visitor.
Create a database client helper. The official guides place this at src/turso.ts and import from the /web entry point:
// src/turso.ts
import { createClient } from "@libsql/client/web";
export const turso = createClient({
url: import.meta.env.TURSO_DATABASE_URL,
authToken: import.meta.env.TURSO_AUTH_TOKEN,
});
Set up your schema. You can run migrations through the Turso CLI or programmatically:
turso db shell my-astro-blog
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
published_at TEXT DEFAULT (datetime('now')),
draft INTEGER DEFAULT 0
);
CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER REFERENCES posts(id),
author TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
Basic Usage
Fetch posts from Turso in your Astro pages:
---
// src/pages/blog/index.astro
import { turso } from "../../turso";
import BaseLayout from "../../layouts/BaseLayout.astro";
const result = await turso.execute(
"SELECT id, title, slug, published_at FROM posts WHERE draft = 0 ORDER BY published_at DESC"
);
const posts = result.rows;
---
<BaseLayout title="Blog">
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
<time>{post.published_at}</time>
</a>
</li>
))}
</ul>
</BaseLayout>
For individual post pages rendered on demand (see the rendering modes section below for why prerender = false matters):
---
// src/pages/blog/[slug].astro
export const prerender = false;
import { turso } from "../../turso";
import BaseLayout from "../../layouts/BaseLayout.astro";
const { slug } = Astro.params;
const postResult = await turso.execute({
sql: "SELECT * FROM posts WHERE slug = ? AND draft = 0",
args: [slug],
});
if (postResult.rows.length === 0) {
return Astro.redirect("/404");
}
const post = postResult.rows[0];
const commentsResult = await turso.execute({
sql: "SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC",
args: [post.id],
});
---
<BaseLayout title={post.title}>
<article>
<h1>{post.title}</h1>
<div set:html={post.content} />
</article>
<section>
<h2>Comments</h2>
{commentsResult.rows.map((comment) => (
<div>
<strong>{comment.author}</strong>
<p>{comment.body}</p>
</div>
))}
</section>
</BaseLayout>
Rendering Modes in Astro 6
This is the part that trips people up, and it changed across Astro versions. In Astro 6 the default output is static. Your entire site is prerendered to HTML at build time unless you opt a route into on-demand rendering. The old output: 'hybrid' value was removed in Astro 5, so any guide that still tells you to set it is out of date. Static is now the baseline and you opt individual routes into server rendering instead.
There are two ways to query Turso, and they map to two different needs:
At build time (static). If your post list rarely changes, leave the page static. The Turso query in
src/pages/blog/index.astroruns once duringastro buildand the result is baked into the HTML. You do not need an adapter for this.On demand (server rendered). If a page must reflect data that changes between deploys, for example a comment count or a freshly published draft, opt that route into on-demand rendering with
export const prerender = false. On-demand rendering requires an adapter to be installed.
Install the Node adapter with the official command, which writes the config and installs @astrojs/node for you (version 10.1.2 at time of writing, which peers Astro ^6.3.0):
npx astro add node
That produces an astro.config.mjs like this:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({
mode: 'standalone',
}),
});
With mode: 'standalone' the build produces a self-contained server you can start directly. Use mode: 'middleware' instead if you are mounting Astro inside an existing Express or Fastify app. Leaving output unset keeps the site static-by-default and lets your prerender = false pages render on demand. Only set output: 'server' if you want every page server rendered by default, in which case you flip the static pages back with export const prerender = true.
Production Tips
Use embedded replicas for fast local reads. Turso supports embedded replicas that sync to a local SQLite file. You point the client at a
file:URL and give it asyncUrl, and reads hit the local file while writes sync back to Turso. The SDK reference shows the shape:import { createClient } from "@libsql/client"; const client = createClient({ url: "file:local.db", syncUrl: "libsql://your-database-name-your-org.turso.io", authToken: import.meta.env.TURSO_AUTH_TOKEN, });Add a
syncInterval(in seconds) for periodic background syncing, or callawait client.sync()manually. Note that embedded replicas use the Node entry point, not@libsql/client/web.
Use parameterized queries. Always pass user input through args instead of string interpolation. This prevents SQL injection and is the documented way to use the libSQL client. libSQL accepts the same named placeholder characters as SQLite (:, @, and $) in addition to positional ?.
Batch related queries. Turso supports batching multiple statements in a single round trip. turso.batch() takes an array of statements plus a transaction mode, for example await turso.batch([...], "write"). The whole batch runs as a transaction, so it rolls back together if any statement fails.
Set up database groups for multi-region. If your audience is global, create a database group with replicas in multiple regions. Turso routes queries to the nearest replica automatically.
Monitor your usage. The current Free plan includes 100 databases, 5GB of total storage, 500 million row reads per month, and 10 million row writes per month. Read-heavy sites tend to sit comfortably inside the free tier. Write-heavy applications should watch the monthly row-write count, since that is the limit you are most likely to hit first.
Alternatives to Consider
- Supabase if you want a full PostgreSQL database with authentication, storage, and real-time subscriptions included.
- PlanetScale if you prefer MySQL and need branching for schema changes in a team environment.
- Neon if you want serverless PostgreSQL with a generous free tier and automatic scaling.
Common Errors and Fixes
Your token shows up in the browser. If you prefix the env vars with PUBLIC_, Astro inlines them into the client bundle. The Astro docs call this out directly. Use TURSO_DATABASE_URL and TURSO_AUTH_TOKEN with no prefix, and read them with import.meta.env only inside server-side code (the frontmatter of an .astro file, an API route, or src/turso.ts).
A page that queries Turso throws at build time or returns stale data. A static page runs its query once during astro build. If you need the data fresh on every request, that route must be on demand. Add export const prerender = false to the page, and make sure an adapter is installed. Without an adapter, Astro 6 has nowhere to run the server code.
output: 'hybrid' is not a valid configuration value. That option was removed in Astro 5. In Astro 6 the default is static, and you opt routes into server rendering with prerender = false (or set output: 'server' and opt routes back out with prerender = true). Delete any leftover output: 'hybrid' line.
Build fails immediately with an engines or syntax error. Astro 6 requires Node 22.12.0 or newer per its engines field. Check node -v and upgrade if you are on 18 or 20.
Server islands need an adapter too. If you move a Turso-backed component to server:defer for a server island, that also requires an adapter to be installed, the same as any on-demand route.
Wrong client entry point for your runtime. Use @libsql/client/web for the standard remote-database setup shown here. Switch to the Node entry point (@libsql/client) when you use embedded replicas with a file: URL, because the web build cannot open a local SQLite file.
Official Docs and Examples
- Turso official Astro guide: https://docs.turso.tech/sdk/ts/guides/astro
- Astro official Turso backend guide: https://docs.astro.build/en/guides/backend/turso/
- Astro on-demand rendering guide: https://docs.astro.build/en/guides/on-demand-rendering/
- Astro Node adapter guide: https://docs.astro.build/en/guides/integrations-guide/node/
- libSQL TypeScript SDK reference: https://docs.turso.tech/sdk/ts/reference
- libSQL source (the SQLite fork Turso is built on): https://github.com/tursodatabase/libsql
Wrapping Up
Turso brings the simplicity of SQLite to the edge. For Astro projects that need a database, whether for comments, user data, or dynamic content, Turso provides a lightweight solution with excellent performance. The free tier is generous, the client library is straightforward, and the edge replication means your data is always close to your users.
Sources
All versions and facts below were checked on 2026-05-29.
- Turso and Astro, official Turso guide (import path
@libsql/client/web, client atsrc/turso.ts, env var names) - Turso and Astro, official Astro guide (install command,
PUBLIC_prefix warning, parameterized query example) - Astro on-demand rendering (static is the default,
prerender = false,output: 'server') - Astro Node adapter (
npx astro add node,standalonevsmiddlewaremodes, config example) - libSQL TypeScript SDK reference (parameterized args,
batch()with"write"mode, embedded replicasyncUrl/syncInterval) - Turso CLI installation (
brew install tursodatabase/tap/turso,curl -sSfL https://get.tur.so/install.sh | bash) - Turso pricing (Free plan: 100 databases, 5GB storage, 500 million row reads, 10 million row writes per month)
- astro on npm (version 6.4.2,
engines.node >=22.12.0) - @libsql/client on npm (version 0.17.3, exports include
./weband./node) - @astrojs/node on npm (version 10.1.2, peers
astro ^6.3.0)
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.