[2026] Astro DB complete guide — Turso, LibSQL, and serverless SQL
이 글의 핵심
Astro DB is the official data layer that ties schema, seeds, and queries to your Astro project on a libSQL-compatible store. This guide covers Turso integration, Drizzle-style queries, local vs remote development, and security when you design a blog-style CMS.
What this article covers
Astro DB delivers a single SQL database setup, development, and deployment story for the Astro ecosystem. Locally it behaves similarly to .astro/content.db, and with providers such as Turso that expose the libSQL remote protocol you can push schemas and keep production aligned with the same model. Drizzle ORM is wired in already: import db, table definitions, and operators from astro:db and use them as you would with Drizzle.
This guide walks through, in order: the role of Astro DB and how it relates to libSQL and Turso; defining schema with defineTable and applying it remotely with astro db push; Drizzle-style select, join, and batch patterns; differences between local and --remote dev and build; authentication, secrets, and input validation when handling user data; and example tables for a blog or lightweight CMS.
1. Core concepts
1-1. What problem Astro DB solves
Traditionally, frontend frameworks forced you to wire “build-time content” and “runtime persistent data” with separate tools. Astro DB uses db/config.ts in the project as the single source of truth so the dev server, seeds, type generation, and schema push at deploy time all follow the same definitions. Renaming tables or columns surfaces TypeScript mismatches immediately and makes it harder for an undocumented “ops-only schema” to drift from the repo.
1-2. libSQL and Turso
libSQL is an open-source embedded and client library derived from SQLite, designed with extensions such as remote access over HTTP or WebSocket and embedded replicas. Turso is a managed platform built on libSQL and is often cited in Astro DB docs as a production connection example. Astro DB is designed so local files, in-memory stores, and remote libSQL servers share the same abstraction.
1-3. What “Drizzle included” means
You do not need extra drizzle-kit boilerplate: use import { db, eq } from 'astro:db' and the Drizzle query builder. Schema is declared with defineTable from astro:db; at runtime you write select, insert, where, innerJoin, and so on in the usual Drizzle style. Think of Astro DB as schema declaration + hosting integration + a Drizzle client.
2. Installation and layout
Add the official @astrojs/db integration per the docs.
npx astro add db
After install, db/config.ts is created at the project root. Define tables there and export with defineDb. Development uses a local database; for seeds, add db/seed.ts.
3. Schema
3-1. Tables and column types
Use defineTable and column to specify types. Common choices include text, integers, booleans, dates, and JSON.
// db/config.ts (conceptual example)
import { defineDb, defineTable, column } from 'astro:db';
const Author = defineTable({
columns: {
id: column.number({ primaryKey: true }),
name: column.text(),
},
});
const Comment = defineTable({
columns: {
id: column.number({ primaryKey: true }),
authorId: column.number({ references: () => Author.columns.id }),
body: column.text(),
},
});
export default defineDb({
tables: { Author, Comment },
});
references points at another table’s key column and helps keep join relationships clear. In real projects, tune column names and types to your domain.
3-2. Seed data
In db/seed.ts, import db and tables from astro:db and run insert. The dev server may re-run seeds when this file changes—a workflow tuned for local iteration. Production data is not mixed in by default; be careful when developing against a remote database.
4. Migrations and schema push
Astro DB sits between “dozens of hand-written migration SQL files” and “push a declarative schema from the CLI.” When applying to a remote database, keep the following in mind.
astro db push --remote: applies changes from localdb/config.tsto the remote libSQL database. When possible, the CLI checks whether the change can be applied without data loss.- Breaking changes: incompatible changes may require
--force-resetto wipe remote data before aligning the schema—use with extreme care in production. - Renaming tables: if a one-step rename is hard, the docs recommend a staged approach: mark the old table
deprecated: true, add a new table with the same shape, push, migrate code and data, then remove the old definition.
When you need to run or transform data from a script, write a TypeScript file that uses the typed astro:db module and run it against the remote with astro db execute ... --remote. Point the seed path at whatever fits your layout.
5. Queries and relational data
5-1. Basic select and filter
From pages, endpoints, or Actions, query with db. Utilities such as eq, like, and and come from astro:db.
import { db, Comment, like } from 'astro:db';
const rows = await db
.select()
.from(Comment)
.where(like(Comment.body, '%keyword%'));
5-2. Joins
Use innerJoin (and related APIs) to combine related tables. If the schema uses references, join conditions stay easier to read.
import { db, eq, Comment, Author } from 'astro:db';
const rows = await db
.select()
.from(Comment)
.innerJoin(Author, eq(Comment.authorId, Author.id));
5-3. Batching and transaction-like usage
Remote databases pay a network cost per request, so the docs recommend db.batch instead of many individual insert calls. If you need rollback semantics, cross-check Drizzle’s transaction APIs with Astro’s runtime constraints.
6. Drizzle ORM integration
import { db } from 'astro:db' is enough—no separate client factory. Import table objects as symbols generated from the schema and follow Drizzle’s select / insert / delete patterns. Utilities (eq, gt, count, sql, etc.) also come from astro:db.
Note: some ecosystem tools (for example auto-schema generators for drizzle-zod) may not fully align with Astro DB’s defineTable types, as reported in the community. For forms, Zod on Action inputs is often the simplest and clearest approach in production.
7. Remote vs local development
7-1. Default behavior
By default, astro dev and astro build use the local database. Tables are created per config and seeds run. You can start without Docker, which is a strong onboarding story.
7-2. Developing and building against a remote DB
Point environment variables at a remote libSQL instance.
ASTRO_DB_REMOTE_URL: e.g. the URL from Turso’sturso db showASTRO_DB_APP_TOKEN: token fromturso db tokens create(required for remote access)
Add --remote to build or dev commands to read and write the remote database—for example astro build --remote, astro dev --remote. On platforms such as Cloudflare Workers you may need web mode for non-Node runtimes; check the official DB integration docs for “mode.”
7-3. URL options and embedded replicas
ASTRO_DB_REMOTE_URL may use schemes such as file:, memory:, libsql:, http(s):, or ws(s):. Query parameters such as encryptionKey, syncUrl, and syncInterval tune encryption and sync cadence with a remote. For read-heavy workloads, consider a local replica alongside Turso and libSQL docs.
8. Authentication and security
8-1. Secrets and tokens
ASTRO_DB_APP_TOKEN must be exposed only on the server and in CI. If it lands in a client bundle or a public env var, anyone can reach your database. Register it as a secret in your host, and keep .env out of git locally.
8-2. Writes only on the server
When users submit forms or APIs, run db.insert and similar only on the server. With Astro Actions, put a Zod schema on defineAction’s input for typed runtime validation. For public endpoints, consider rate limits, bot protection, and session checks.
8-3. Authorization model
SQLite-family databases do not ship with PostgreSQL-style Row Level Security by default. Who may change which rows is usually enforced in the app: after session, JWT, or cookie validation, constrain queries with where on ownership. Admin UIs should use separate authentication and roles.
9. In practice: blog or lightweight CMS
9-1. Example tables
For a blog, use Post, Author, Category, and a junction such as PostCategory if you need many-to-many. For comments, add Comment with postId.
// Concept sketch — adjust columns to your requirements
import { defineDb, defineTable, column } from 'astro:db';
const Author = defineTable({
columns: {
id: column.number({ primaryKey: true }),
slug: column.text(),
displayName: column.text(),
},
});
const Post = defineTable({
columns: {
id: column.number({ primaryKey: true }),
slug: column.text({ unique: true }),
title: column.text(),
body: column.text(),
publishedAt: column.date(),
authorId: column.number({ references: () => Author.columns.id }),
},
});
export default defineDb({ tables: { Author, Post } });
Confirm support for options such as unique in the official table and column reference.
9-2. Separating reads and writes
- Public listing and post detail: generate statically at build time or use SSR with
db.selectand set cache headers. - Admin drafts and publishing: only authenticated users
insert/updatevia Actions. When storing Markdown, define a policy for format (plain text vs sanitized HTML) to reduce XSS risk.
9-3. Moving from Astro Studio to Turso
If you used Astro Studio, the docs describe creating a database on Turso, aligning environment variables, running astro db push --remote, and importing data with tools such as turso db shell. After migration, decide how to retire Studio per team policy.
10. Troubleshooting quick reference
| Symptom | What to check |
|---|---|
| Push rejected | Breaking schema change? Back up production before --force-reset, or use staged deprecation |
| Cannot write remotely | ASTRO_DB_* vars, --remote on build/dev, adapter and mode settings |
| Type errors | Restart after db/config.ts changes; table import casing |
| Performance | Reduce N+1, use batch and joins to cut round trips; add caching if needed |
11. Summary
Astro DB locks schema in code and connects cleanly to libSQL hosts such as Turso instead of growing parallel Drizzle config files. Focusing on astro:db keeps reads, writes, and deploy scripts on the same model inside your Astro app. The habits that matter most in production: fast iteration locally, and remote flags plus secrets and server-side validation when you go live.
Before deploy, run git add, commit, and push, then npm run deploy (per your project scripts), and double-check that environment variables are set in your hosting console.