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.
Step 1: Create a Supabase Project
- Go to supabase.com and create an account
- Click New Project
- Choose an organization, name, database password, and region
- Wait ~2 minutes for the project to initialize
Step 2: Get API Credentials
- In your project, go to Project Settings (gear icon, left sidebar)
- Click API
- Copy the Project URL — looks like
https://xyzcompany.supabase.co - Under Project API keys, copy the
service_rolekey
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.
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
[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):
SUPABASE_URL=https://xyzcompany.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
.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:
<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
<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:
<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:
| Syntax | Description | Example |
|---|---|---|
field=value | Exact match | breed=Labrador |
field!=value | Not equal | breed!=Poodle |
field>value | Greater than | age>2 |
field<value | Less than | age<10 |
field~value | Contains (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;