Step 7 — Database & CRUD
Full create, read, update, delete — from HTML forms, no backend code.
How data fetching works
Declare a fetch.* directive in the <what> block. The local: prefix queries your configured database. The result is available as a template variable.
<what>
fetch.notes = "local:notes?sort=created_at:desc&limit=20"
</what>
<loop data="#notes#" as="note">
<div>#note.title#</div>
</loop>
Create a note
Use action="/w-action/notes" with method="post" to insert a new record. The form fields become the record's columns.
Your notes
Records are fetched via fetch.notes = "local:notes" in the <what> block at the top of this page.
No notes yet. Create one above.
Real per-visitor ownership, enforced
You share this page with every other visitor, yet you only see and can delete
your own notes. That's a one-line authorization policy in
what.toml:
[collections.tut_notes] read = "owner" (plus update/delete).
The framework stamps an owner on each note when it's created and enforces it on every
read and mutation — another visitor can't fetch or delete your notes even by guessing the id.
No hidden fields, no manual filtering.
CRUD reference
| Action | Form action | Method |
|---|---|---|
| Create record | /w-action/notes |
POST |
| Update record | /w-action/notes/#id# |
POST |
| Delete record | /w-action/notes/#id#?w-action=delete |
POST |
Where does the data go?
Forms can send data to different destinations depending on the action URL:
| Destination | How |
|---|---|
| Database | action="/w-action/collection" — persists to SQLite/D1 |
| Session | w-set="session.key = $value" — per-user, no form submit needed |
| App state | w-set="app.key = $value" — shared across all users |
| Wired (real-time) | w-set="wired.key = $value" — broadcasts to all browsers via WebSocket |
CSRF protection
Every <form method="post"> automatically gets a hidden CSRF token injected by the engine. You don't need to add it manually — it's built in.
<!-- You write this: -->
<form method="post" action="/w-action/notes">
<input name="title">
<button type="submit">Save</button>
</form>
<!-- The engine renders this: -->
<form method="post" action="/w-action/notes">
<input type="hidden" name="_csrf" value="auto-generated-token">
<input name="title">
<button type="submit">Save</button>
</form>
Security: POST requests without a valid CSRF token are rejected with 403 Forbidden. This prevents cross-site request forgery attacks.
Query options
<!-- Sort, filter, search, paginate -->
fetch.notes = "local:notes?sort=created_at:desc&limit=10&offset=0"
fetch.active = "local:items?filter=status:active"
fetch.results = "local:posts?search=hello"
What you learned
fetch.name = "local:collection"queries your SQLite database- Fetched data is available as
#name#and iterated with<loop> - Create:
POST /w-action/collection— form fields become columns - Update:
POST /w-action/collection/#id# - Delete:
POST /w-action/collection/#id#?w-action=delete - Query params:
sort,filter,search,limit,offset - Data can go to: database (
/w-action/), session, app state, or wired (real-time) - CSRF tokens are auto-injected into all POST forms — no manual setup needed