what

Cloudflare D1

Use Cloudflare's serverless SQLite database as your app's data backend. Data lives on Cloudflare's edge network and survives all redeploys automatically.

What is D1?

D1 is Cloudflare's managed SQLite database. It runs at the edge, has a generous free tier (5 GB storage, 5 million reads/day), and requires zero infrastructure management. Your data is cloud-hosted, so it persists across redeploys, container restarts, and server migrations.

The What framework connects to D1 via the Cloudflare REST API. Your templates and form actions work identically to SQLite — switching backends is a config change, not a code change.

When to use D1: Any time you need your data to survive redeploys without manual volume management. D1 is the recommended backend for production apps on Hetzner, Digital Ocean, or any VPS.

Step 1: Create a D1 Database

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

Step 2: Get API Credentials

  1. Click your profile icon (top right) → My Profile
  2. Go to API Tokens → Create Token
  3. Use the Edit Cloudflare Workers template, or create a custom token with Account → D1 → Edit permission
  4. Copy the API Token
  5. Your Account ID is in the dashboard URL: dash.cloudflare.com/[ACCOUNT_ID]/...

Step 3: Create Tables

System tables (_kv_store and _collections) are created automatically when What starts. You only need to create your own data tables.

Run this SQL in the D1 console (Workers & Pages → D1 → your database → Console):

Real Columns (Recommended)

Define your table with standard SQL columns. The What framework auto-detects the schema and reads columns directly:

CREATE TABLE IF NOT EXISTS dogs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  breed TEXT,
  age INTEGER DEFAULT 0,
  is_good_boy INTEGER DEFAULT 1,
  metadata TEXT  -- stores JSON if needed
);
INSERT OR IGNORE INTO _collections (name) VALUES ('dogs');

Each column maps directly to a template variable: #dog.name#, #dog.breed#, #dog.age#, etc.

JSON Blob Mode (Legacy)

Tables with only id and data columns store all fields as a JSON string in the data column:

CREATE TABLE IF NOT EXISTS dogs (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  data TEXT NOT NULL DEFAULT '{}'
);
INSERT OR IGNORE INTO _collections (name) VALUES ('dogs');

The What framework auto-detects which mode each table uses. Real columns are recommended for new projects — they give you SQL-level filtering, sorting, and type safety.

Auto-detection: If a table has exactly two columns named id and data, What uses JSON blob mode. Any other column layout uses real column mode. This detection is cached per table for performance.

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"

Set your credentials as environment variables. Create a .env file at your project root (never commit this):

.env
CF_ACCOUNT_ID=your-account-id-here
CF_API_TOKEN=your-api-token-here
Security: Add .env to your .gitignore. Never commit API tokens to source control.

List Records

Fetch and display dogs using the local: prefix, exactly as you would with SQLite:

site/dogs/index.html
<what>
fetch.dogs = "local:dogs"
fetch.dogs.sort = "created_at:desc"
</what>

<h1>Dogs</h1>

<if flash.success>
  <div class="alert alert-success mb-4">#flash.success#</div>
</if>

<if dogs>
  <loop data="#dogs#" as="dog">
    <div class="card mb-3">
      <div class="card-body d-flex justify-content-between align-items-center">
        <div>
          <strong>#dog.name#</strong>
          <span class="text-muted ml-2">#dog.breed#</span>
          <span class="badge ml-2">Age: #dog.age#</span>
        </div>
        <div class="d-flex gap-2">
          <a href="/dogs/#dog.id#/edit" class="btn btn-sm">Edit</a>
          <form w-action="delete" w-store="dogs" w-id="#dog.id#" w-redirect="/dogs">
            <button type="submit" class="btn btn-sm btn-danger"
                    w-confirm="Delete #dog.name#?">Delete</button>
          </form>
        </div>
      </div>
    </div>
  </loop>
</if>
<else/>
  <p class="text-muted">No dogs yet.</p>
</else>

<a href="/dogs/new" class="btn btn-primary mt-3">Add Dog</a>

Create a Record

site/dogs/new.html
<h1>Add a Dog</h1>

<if errors.name>
  <div class="alert alert-danger">#errors.name#</div>
</if>

<form w-action="create" w-store="dogs" w-redirect="/dogs" w-validate class="card card-body">
  <div class="mb-3">
    <label class="form-label">Name</label>
    <input name="name" type="text" class="form-control"
           placeholder="Dog's name" w-required value="#old.name#">
    <if errors.name>
      <div class="form-text text-danger">#errors.name#</div>
    </if>
  </div>

  <div class="mb-3">
    <label class="form-label">Breed</label>
    <input name="breed" type="text" class="form-control"
           placeholder="e.g. Labrador" value="#old.breed#">
  </div>

  <div class="mb-3">
    <label class="form-label">Age</label>
    <input name="age" type="number" class="form-control"
           placeholder="Years" w-min="0" w-max="30" value="#old.age#">
  </div>

  <button type="submit" class="btn btn-primary">Add Dog</button>
  <a href="/dogs" class="btn ml-2">Cancel</a>
</form>

Update a Record

Fetch the existing record by ID and pre-fill the form:

site/dogs/[id]/edit.html
<what>
fetch.dog = "local:dogs"
fetch.dog.filter = "id=#params.id#"
</what>

<h1>Edit #dog.name#</h1>

<form w-action="update" w-store="dogs" w-id="#dog.id#" w-redirect="/dogs" class="card card-body">
  <div class="mb-3">
    <label class="form-label">Name</label>
    <input name="name" type="text" class="form-control" value="#dog.name#">
  </div>

  <div class="mb-3">
    <label class="form-label">Breed</label>
    <input name="breed" type="text" class="form-control" value="#dog.breed#">
  </div>

  <div class="mb-3">
    <label class="form-label">Age</label>
    <input name="age" type="number" class="form-control" value="#dog.age#">
  </div>

  <button type="submit" class="btn btn-primary">Save Changes</button>
  <a href="/dogs" class="btn ml-2">Cancel</a>
</form>

Delete a Record

Inline delete with a confirmation dialog. No separate page needed:

<form w-action="delete" w-store="dogs" w-id="#dog.id#" w-redirect="/dogs">
  <button type="submit" class="btn btn-danger"
          w-confirm="Permanently delete #dog.name#? This cannot be undone.">
    Delete Dog
  </button>
</form>

Query Options

The local: fetch prefix supports sort, filter, search, limit, and offset:

<what>
<!-- Sort by name ascending -->
fetch.dogs = "local:dogs"
fetch.dogs.sort = "name:asc"

<!-- Filter by exact field value -->
fetch.labs = "local:dogs"
fetch.labs.filter = "breed=Labrador"

<!-- Full-text search across all fields -->
fetch.results = "local:dogs"
fetch.results.search = "#params.q#"

<!-- Pagination: 10 per page -->
fetch.dogs = "local:dogs"
fetch.dogs.limit = "10"
fetch.dogs.offset = "#params.offset|default:0#"
</what>

Filter operators:

SyntaxDescriptionExample
field=valueExact matchbreed=Labrador
field!=valueNot equalbreed!=Poodle
field>valueGreater thanage>2
field<valueLess thanage<10
field~valueContains (case-insensitive)name~buddy
Tip: D1's free tier includes 5 GB of storage and 5 million read rows per day. That's more than enough for most production apps. See the D1 limits documentation for details.

Using D1 as a Named Datasource

Instead of (or in addition to) the [database] config, you can configure D1 as a named datasource using the dsn: prefix:

what.toml
[datasources.cloud]
type = "d1"
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
d1_database_id = "${CF_D1_DATABASE_ID}"
site/todos/index.html
<what>
[data]
fetch.todos = "dsn:cloud.todos"
fetch.todos.sort = "created_at:desc"
</what>

<loop data="#todos#" as="todo">
  <p>#todo.title#</p>
</loop>

This lets you connect to multiple D1 databases simultaneously, or mix D1 with other backends like Supabase or REST APIs.