what

Supabase

Use Supabase as your app's PostgreSQL backend. Full SQL power, a built-in dashboard, and a free tier — with zero changes to your What templates.

What is Supabase?

Supabase is a hosted PostgreSQL platform with a REST API (PostgREST), a visual table editor, and a generous free tier (500 MB storage). Your data lives in their cloud and survives all redeploys automatically.

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

When to use Supabase: When you want PostgreSQL's data types and constraints, a visual table editor to browse your data, or you're already using Supabase for other services.

Step 1: Create a Supabase Project

  1. Go to supabase.com and create an account
  2. Click New Project
  3. Choose an organization, name, database password, and region
  4. Wait ~2 minutes for the project to initialize

Step 2: Get API Credentials

  1. In your project, go to Project Settings (gear icon, left sidebar)
  2. Click API
  3. Copy the Project URL — looks like https://xyzcompany.supabase.co
  4. Under Project API keys, copy the service_role key
Use the service_role key, not the anon key. The service_role key bypasses Row Level Security so your server has full access to the database. Never expose it in client-side code — it belongs only in your .env file on the server.

Step 3: Create Tables

The What framework stores collection data in a standard schema. Go to SQL Editor in your Supabase dashboard and run this once per project:

-- System tables (run 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);

Then create a table for each collection your app uses. For a dogs collection:

CREATE TABLE IF NOT EXISTS dogs (
  id BIGSERIAL PRIMARY KEY,
  data JSONB NOT NULL DEFAULT '{}'
);
INSERT INTO _collections (name) VALUES ('dogs') ON CONFLICT DO NOTHING;

Repeat the CREATE TABLE + INSERT INTO _collections pair for each collection. Collection names match the store name you use in w-store and local: fetch directives.

Row Level Security: The What framework uses the service_role key which bypasses RLS. You do not need to create RLS policies for What to function. If you also use Supabase's client-side libraries, configure RLS separately for those access paths.

Step 4: Configure what.toml

what.toml
[database]
type = "supabase"

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

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

.env
SUPABASE_URL=https://xyzcompany.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Security: Add .env to your .gitignore. Never commit API keys 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>

<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

Using the Supabase Dashboard

One advantage of Supabase is the visual table editor. Go to Table Editor in your Supabase dashboard to browse records, run queries, and manage data directly — no command line needed.

Your data lives in the data JSONB column of each table. The Supabase dashboard displays it as a JSON object. You can also use the SQL Editor to run arbitrary queries against your data:

-- Find all dogs named Buddy
SELECT id, data->>'name' as name, data->>'breed' as breed
FROM dogs
WHERE data->>'name' = 'Buddy';

-- Count dogs by breed
SELECT data->>'breed' as breed, COUNT(*) as count
FROM dogs
GROUP BY data->>'breed'
ORDER BY count DESC;
Tip: Supabase's free tier includes 500 MB of database storage and 2 GB of bandwidth per month. You can manage and inspect your data directly through the Supabase dashboard's Table Editor and SQL Editor.