<what>
title: "Step 7 — Database & CRUD"
active_step: 7
fetch.notes = "local:notes?sort=created_at:desc"
</what>
<h1 class="text-3xl font-bold mb-2" style="color: #111827; letter-spacing: -0.02em;">Step 7 — Database & CRUD</h1>
<p class="text-gray-500 text-sm mb-8">Full create, read, update, delete — from HTML forms, no backend code.</p>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">How data fetching works</h2>
<p class="text-gray-600 mb-4" style="line-height: 1.7;">
Declare a <code>fetch.*</code> directive in the <code><what></code> block. The <code>local:</code> prefix queries your configured database. The result is available as a template variable.
</p>
<pre class="bg-gray-50 p-4 rounded text-sm font-mono" style="border: 1px solid #e5e7eb; overflow-x: auto;"><code><what>
fetch.notes = "local:notes?sort=created_at:desc&limit=20"
</what>
<loop data="#notes#" as="note">
<div>#note.title#</div>
</loop></code></pre>
</section>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">Create a note</h2>
<p class="text-gray-600 mb-3" style="line-height: 1.7;">
Use <code>action="/w-action/notes"</code> with <code>method="post"</code> to insert a new record. The form fields become the record's columns.
</p>
<if flash.success>
<div class="alert alert-success mb-4">#flash.success#</div>
</if>
<if flash.error>
<div class="alert alert-danger mb-4">#flash.error#</div>
</if>
<div class="card mb-6">
<div class="card-body">
<form method="post" action="/w-action/notes?w-redirect=/tutorial/7" w-validate>
<div class="form-group">
<label class="form-label" for="note-title">Title <span style="color: #ef4444;">*</span></label>
<input
type="text"
id="note-title"
name="title"
class="form-control"
placeholder="Note title..."
w-required
value="#old.title#"
autocomplete="off"
data-1p-ignore
>
<if errors.title>
<div class="text-sm mt-1" style="color: #dc2626;">#errors.title#</div>
</if>
</div>
<div class="form-group">
<label class="form-label" for="note-body">Content</label>
<textarea
id="note-body"
name="body"
class="form-control"
rows="3"
placeholder="Write something..."
autocomplete="off"
data-1p-ignore
>#old.body#</textarea>
</div>
<div class="flex gap-2">
<button type="submit" class="btn btn-primary">Save note</button>
<button type="reset" class="btn btn-outline">Cancel</button>
</div>
</form>
</div>
</div>
</section>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">Your notes</h2>
<p class="text-gray-600 mb-3" style="line-height: 1.7;">
Records are fetched via <code>fetch.notes = "local:notes"</code> in the <code><what></code> block at the top of this page.
</p>
<unless notes>
<div class="card" style="border-style: dashed;">
<div class="card-body text-center text-gray-400 py-8">
No notes yet. Create one above.
</div>
</div>
</unless>
<loop data="#notes#" as="note">
<div class="card mb-3">
<div class="card-body">
<div class="flex justify-between items-start">
<div style="flex: 1;">
<div class="font-semibold text-gray-900 mb-1">#note.title#</div>
<if note.body>
<div class="text-sm text-gray-600" style="line-height: 1.6;">#note.body#</div>
</if>
</div>
<form method="post" action="/w-action/notes/#note.id#?w-action=delete&w-redirect=/tutorial/7" style="margin-left: 1rem; flex-shrink: 0;">
<button type="submit" class="btn btn-sm" style="background: #fef2f2; color: #dc2626; border: 1px solid #fecaca;">Delete</button>
</form>
</div>
</div>
</div>
</loop>
</section>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">CRUD reference</h2>
<div class="card">
<div class="card-body" style="padding: 0;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">
<thead>
<tr style="background: #f9fafb; border-bottom: 1px solid #e5e7eb;">
<th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Action</th>
<th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Form action</th>
<th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Method</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #f3f4f6;">
<td style="padding: 0.625rem 1rem;">Create record</td>
<td style="padding: 0.625rem 1rem;"><code>/w-action/notes</code></td>
<td style="padding: 0.625rem 1rem;"><code>POST</code></td>
</tr>
<tr style="border-bottom: 1px solid #f3f4f6;">
<td style="padding: 0.625rem 1rem;">Update record</td>
<td style="padding: 0.625rem 1rem;"><code>/w-action/notes/#id#</code></td>
<td style="padding: 0.625rem 1rem;"><code>POST</code></td>
</tr>
<tr>
<td style="padding: 0.625rem 1rem;">Delete record</td>
<td style="padding: 0.625rem 1rem;"><code>/w-action/notes/#id#?w-action=delete</code></td>
<td style="padding: 0.625rem 1rem;"><code>POST</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">Where does the data go?</h2>
<p class="text-gray-600 mb-3" style="line-height: 1.7;">
Forms can send data to different destinations depending on the action URL:
</p>
<div class="card mb-4">
<div class="card-body" style="padding: 0;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;">
<thead>
<tr style="background: #f9fafb; border-bottom: 1px solid #e5e7eb;">
<th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">Destination</th>
<th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151;">How</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #f3f4f6;">
<td style="padding: 0.625rem 1rem;">Database</td>
<td style="padding: 0.625rem 1rem; color: #6b7280;"><code>action="/w-action/collection"</code> — persists to SQLite/D1</td>
</tr>
<tr style="border-bottom: 1px solid #f3f4f6;">
<td style="padding: 0.625rem 1rem;">Session</td>
<td style="padding: 0.625rem 1rem; color: #6b7280;"><code>w-set="session.key = $value"</code> — per-user, no form submit needed</td>
</tr>
<tr style="border-bottom: 1px solid #f3f4f6;">
<td style="padding: 0.625rem 1rem;">App state</td>
<td style="padding: 0.625rem 1rem; color: #6b7280;"><code>w-set="app.key = $value"</code> — shared across all users</td>
</tr>
<tr>
<td style="padding: 0.625rem 1rem;">Wired (real-time)</td>
<td style="padding: 0.625rem 1rem; color: #6b7280;"><code>w-set="wired.key = $value"</code> — broadcasts to all browsers via WebSocket</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">CSRF protection</h2>
<p class="text-gray-600 mb-3" style="line-height: 1.7;">
Every <code><form method="post"></code> automatically gets a hidden CSRF token injected by the engine. You don't need to add it manually — it's built in.
</p>
<pre class="bg-gray-50 p-4 rounded text-sm font-mono" style="border: 1px solid #e5e7eb; overflow-x: auto;"><code><!-- 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></code></pre>
<div class="alert" style="margin-top: 1rem;">
<strong>Security:</strong> POST requests without a valid CSRF token are rejected with 403 Forbidden. This prevents cross-site request forgery attacks.
</div>
</section>
<section class="mb-8">
<h2 class="text-lg font-semibold mb-3" style="color: #111827;">Query options</h2>
<pre class="bg-gray-50 p-4 rounded text-sm font-mono" style="border: 1px solid #e5e7eb; overflow-x: auto;"><code><!-- 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"</code></pre>
</section>
<div class="card" style="border-color: #bbf7d0; background: #f0fdf4;">
<div class="card-body">
<div class="text-sm font-semibold mb-2" style="color: #14532d;">What you learned</div>
<ul class="text-sm space-y-1" style="color: #166534; padding-left: 1.25rem; list-style: disc;">
<li><code>fetch.name = "local:collection"</code> queries your SQLite database</li>
<li>Fetched data is available as <code>#name#</code> and iterated with <code><loop></code></li>
<li>Create: <code>POST /w-action/collection</code> — form fields become columns</li>
<li>Update: <code>POST /w-action/collection/#id#</code></li>
<li>Delete: <code>POST /w-action/collection/#id#?w-action=delete</code></li>
<li>Query params: <code>sort</code>, <code>filter</code>, <code>search</code>, <code>limit</code>, <code>offset</code></li>
<li>Data can go to: database (<code>/w-action/</code>), session, app state, or wired (real-time)</li>
<li>CSRF tokens are auto-injected into all POST forms — no manual setup needed</li>
</ul>
</div>
</div>