what

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).

Tip: Sessions are stored in a separate SQLite file (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.

site/blog.html
<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:

BackendWhere Data LivesSurvives 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
Important: When deploying with Docker, always mount your 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

  1. Log in to the Cloudflare Dashboard
  2. Go to Workers & Pages → D1 SQL Database
  3. Click Create, name it (e.g., my-app-db)
  4. Copy the Database ID (a UUID like a1b2c3d4-...)

Step 2: Get Your API Credentials

  1. Go to My Profile → API Tokens (top right menu)
  2. Click Create Token
  3. Use the Edit Cloudflare D1 template, or create a custom token with:
    Account → D1 → Edit permission
  4. Copy the API Token
  5. 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

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

.env
CF_ACCOUNT_ID=your-account-id-here
CF_API_TOKEN=your-api-token-here
Tip: D1 has a generous free tier (5 GB storage, 5 million reads/day). It's ideal for small-to-medium apps deployed on any VPS.

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

  1. Go to supabase.com and create an account
  2. Click New Project, choose a name and region
  3. Wait for the project to initialize (takes ~2 minutes)

Step 2: Get Your API Credentials

  1. Go to Project Settings → API (left sidebar, gear icon)
  2. Copy the Project URL (looks like https://xyzcompany.supabase.co)
  3. Under Project API keys, copy the service_role key (the secret one, NOT the anon key)
Important: Use the 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

what.toml
[database]
type = "supabase"

[supabase]
project_url = "${SUPABASE_URL}"
api_key = "${SUPABASE_SERVICE_KEY}"

Step 5: Set Environment Variables

.env
SUPABASE_URL=https://xyzcompany.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Tip: Supabase's free tier includes 500 MB of database storage and unlimited API requests. You can manage your data directly through the Supabase dashboard's Table Editor.

Choosing a Backend

BackendBest ForData PersistenceCost
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.