Payload has no "Turso adapter". Turso is SQLite on a server, and Payload already knows how to talk to SQLite, so you connect with the same adapter you would use with a local file. This is what we do on this site, no detours.
First, a quick word on what Turso is and when it makes sense. It is a database based on libSQL (a fork of SQLite) that you reach over the network with a URL and a token. If you want to compare it with Postgres or MongoDB, see which databases Payload CMS supports.
Step 1: create the database in Turso
With the Turso CLI installed (it is in their documentation):
turso auth login
turso db create my-site
turso db show my-site --url
turso db tokens create my-siteThe third command gives you a URL that starts with libsql://. The fourth gives you a token. Save both. If you want it to expire, turso db tokens create accepts --expiration, and --read-only creates a read-only one, useful for a replica or a query script.
Step 2: install the adapter
pnpm add @payloadcms/db-sqliteStep 3: configure Payload
In payload.config.ts:
import { sqliteAdapter } from '@payloadcms/db-sqlite';
export default buildConfig({
// …
db: sqliteAdapter({
client: {
url: process.env.DATABASE_URL ?? 'file:./data/payload.db',
authToken: process.env.DATABASE_AUTH_TOKEN,
},
}),
});And in .env:
# Local
DATABASE_URL=file:./data/payload.db
# Production
# DATABASE_URL=libsql://my-site-your-user.turso.io
# DATABASE_AUTH_TOKEN=…The same code works for both environments. Locally, file: opens a SQLite file and no token is needed. In production, libsql:// plus the token point to Turso. There is no if in the code, only the variables change.
Step 4: develop with a file, no network
In development, the adapter syncs the schema on its own every time you change a collection. It is convenient: you edit a field and the table updates. You will see a row called dev in the payload_migrations table; it marks that the database has been adjusted this way, and it is normal.
Working against a local file has another advantage: you spend no network or Turso quota while developing, and you can delete the database and seed it again in seconds.
Step 5: migrations for production
That automatic adjustment is not what you want in production. There you use migrations, which are versioned files in Git. When you finish a schema change:
pnpm payload migrate:create change-nameIt generates a file in src/migrations/ with the SQL statements. You review it, ship it with your code and on deploy it is applied:
pnpm payload migrateIn our package.json the Vercel build script does both, back to back:
"vercel-build": "payload migrate && next build"So every deploy applies the pending migrations against Turso before building. For that to work, the build environment needs DATABASE_URL and DATABASE_AUTH_TOKEN, not just the runtime one. It is the most common oversight.
What we have had to watch out for
- One process at a time on the local file. If the dev server is open and you run a seed script against the same database, it can fail with
disk I/O error. We use a separate database for those tests. - Don't mix
pushand migrations on the same database. If you already applied changes with the automatic development adjustment and then create the migration, the local database and the file may not match. When in doubt, recreate the local database from scratch. - Read the SQL of every migration. Especially if you rename or drop columns: SQLite handles those changes with less freedom than Postgres.
When not to use Turso with Payload
SQLite allows one writer at a time. For a content site, where you write little and read a lot, that is not a problem. For an application with many concurrent writes (orders, sessions, real-time events), Postgres is the more comfortable choice.
To see how all this fits in a real site, how codegea.com is built covers the full stack.
Written by
Derian Pinto
Founder & Full Stack Developer
Founder of Codegea. Builds custom websites, stores and platforms, and integrates and supports Arc XP for media companies.