Database
Store and query structured data with SQLite (default), Cloudflare D1, or Supabase. All backends use the same local: fetch syntax and form actions.
How Data Works
The What framework stores all collection data as JSON blobs inside database tables. Every backend uses the same schema:
-- Each collection (e.g., "posts") becomes a table:
id INTEGER PRIMARY KEY AUTOINCREMENT
data TEXT NOT NULL -- JSON object with all fields
-- Key-value pairs use a system table:
_kv_store (key TEXT PRIMARY KEY, value TEXT)
This means your templates and forms work identically regardless of which backend you use. Switching backends is a config change, not a code change.
Default: SQLite (Zero Config)
With no [database] section in what.toml, What automatically creates a SQLite database at data/app.db. No configuration needed.
your-project/
data/
app.db <!-- auto-created on first run -->
sessions.db <!-- separate file for sessions -->
site/
what.toml <!-- no [database] section needed -->
SQLite is the default backend and works great for development and single-server deployments. Tables are created automatically the first time you write data — no configuration needed. Data persists across restarts but is not guaranteed to survive redeploys unless you explicitly preserve the data/ directory (via Docker volume mount or rsync exclude).
sessions.db) from your app data. They are independent and always use SQLite locally.
Querying Data
All data access goes through fetch directives with the local: prefix. You never write SQL directly.
<what>
fetch.posts = "local:posts"
fetch.posts.filter = "status=published"
fetch.posts.sort = "published_at:desc"
fetch.posts.limit = "20"
</what>
<loop data="#posts#" as="post">
<article>
<h2><a href="/blog/#post.id#">#post.title#</a></h2>
<time>#post.published_at|date:"%B %d, %Y"#</time>
</article>
</loop>
Create, update, and delete records through form actions:
<form w-action="create" w-store="posts">
<input name="title" w-required>
<textarea name="body"></textarea>
<button type="submit">Create Post</button>
</form>
Data Persistence Across Redeploys
Understanding what survives a redeploy depends on your database backend and deployment method:
| Backend | Where Data Lives | Survives Redeploy? | How to Ensure Persistence |
|---|---|---|---|
| SQLite | data/app.db on disk |
Yes, if data/ is preserved |
Docker volume mount or rsync exclude |
| Sessions | data/sessions.db on disk |
Yes, if data/ is preserved |
Same as SQLite — mount data/ |
| Cloudflare D1 | Cloudflare edge network | Always | Cloud-hosted, no action needed |
| Supabase | Supabase PostgreSQL cloud | Always | Cloud-hosted, no action needed |
| Uploads | uploads/ on disk |
Yes, if uploads/ is preserved |
Docker volume mount or rsync exclude |
data/ directory as a volume. Without this, your database is destroyed on every container restart. See Deployment for exact commands.
Cloudflare D1
D1 is Cloudflare's serverless SQL database. Your data lives on Cloudflare's edge network and survives all redeploys automatically.
Step 1: Create a D1 Database
- Log in to the Cloudflare Dashboard
- Go to Workers & Pages → D1 SQL Database
- Click Create, name it (e.g.,
my-app-db) - Copy the Database ID (a UUID like
a1b2c3d4-...)
Step 2: Get Your API Credentials
- Go to My Profile → API Tokens (top right menu)
- Click Create Token
- Use the Edit Cloudflare D1 template, or create a custom token with:
Account → D1 → Editpermission - Copy the API Token
- Your Account ID is in the URL:
dash.cloudflare.com/[ACCOUNT_ID]/...
Step 3: Create Tables in D1
In your D1 database console, run this SQL for each collection your app uses:
-- System tables (required once)
CREATE TABLE IF NOT EXISTS _kv_store (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS _collections (name TEXT PRIMARY KEY);
-- Create a collection table (repeat for each collection)
CREATE TABLE IF NOT EXISTS "posts" (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT NOT NULL DEFAULT '{}');
INSERT OR IGNORE INTO _collections (name) VALUES ('posts');
Step 4: Configure what.toml
[database]
type = "d1"
[cloudflare]
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
d1_database_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Step 5: Set Environment Variables
Create a .env file (never commit this):
CF_ACCOUNT_ID=your-account-id-here
CF_API_TOKEN=your-api-token-here
Supabase
Supabase provides a hosted PostgreSQL database with a REST API. Your data is cloud-hosted and survives all redeploys automatically.
Step 1: Create a Supabase Project
- Go to supabase.com and create an account
- Click New Project, choose a name and region
- Wait for the project to initialize (takes ~2 minutes)
Step 2: Get Your API Credentials
- Go to Project Settings → API (left sidebar, gear icon)
- Copy the Project URL (looks like
https://xyzcompany.supabase.co) - Under Project API keys, copy the
service_rolekey (the secret one, NOT theanonkey)
service_role key, not the anon key. The service_role key bypasses Row Level Security and gives your server full access to the database. Never expose this key in client-side code.
Step 3: Create Tables in Supabase
Go to SQL Editor in your Supabase dashboard and run this for each collection:
-- System tables (required once)
CREATE TABLE IF NOT EXISTS _kv_store (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS _collections (name TEXT PRIMARY KEY);
-- Create a collection table (repeat for each collection)
CREATE TABLE IF NOT EXISTS "posts" (id BIGSERIAL PRIMARY KEY, data JSONB NOT NULL DEFAULT '{}');
INSERT INTO _collections (name) VALUES ('posts') ON CONFLICT DO NOTHING;
Step 4: Configure what.toml
[database]
type = "supabase"
[supabase]
project_url = "${SUPABASE_URL}"
api_key = "${SUPABASE_SERVICE_KEY}"
Step 5: Set Environment Variables
SUPABASE_URL=https://xyzcompany.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Choosing a Backend
| Backend | Best For | Data Persistence | Cost |
|---|---|---|---|
| SQLite (default) | Development, prototyping, single-server | Local file — not guaranteed across deploys | Free |
| Cloudflare D1 | Edge performance, serverless | Cloud — always persists | Free tier (5 GB) |
| Supabase | Full PostgreSQL, dashboard UI, scaling | Cloud — always persists | Free tier (500 MB) |
All backends use the same local: fetch syntax and form actions. Your templates don't change when you switch backends.