# Payload CMS with Turso step by step

> How to connect Payload CMS to Turso: create the database, configure the SQLite adapter, work with a local file and run migrations in production.

URL: https://codegea.com/en/payload-cms-with-turso-step-by-step
CMS · 2026-10-18 · Derian Pinto

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](/en/which-databases-payload-cms-supports).

## Step 1: create the database in Turso

With the Turso CLI installed (it is in their documentation):

```bash
turso auth login
turso db create my-site
turso db show my-site --url
turso db tokens create my-site
```

The 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

```bash
pnpm add @payloadcms/db-sqlite
```

## Step 3: configure Payload

In `payload.config.ts`:

```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`:

```bash
# 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:

```bash
pnpm payload migrate:create change-name
```

It generates a file in `src/migrations/` with the SQL statements. You review it, ship it with your code and on deploy it is applied:

```bash
pnpm payload migrate
```

In our `package.json` the Vercel build script does both, back to back:

```json
"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 ****`push`**** and 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](/en/how-codegea-is-built-nextjs-payload-turso-r2) covers the full stack.
