Skip to main content
The board from part 1 forgets everything when you reload. This part gives it a database and makes the buttons work, and almost none of that involves JavaScript. Creating and deleting cards is the kind of interaction that usually pulls in a client-side framework. HTMX handles it by requesting a fragment of HTML from the server and swapping that fragment into the page, which keeps the logic in Python.

What you’ll have

  • Columns and cards stored in SQLite
  • Create and delete for both, with no page reload
  • A confirmation step before anything is destroyed
  • Data that’s still there after a refresh

Which parts of Feather this uses

Models and mixins

UUIDMixin and TimestampMixin supply the columns every model ends up wanting.

Services

Business logic, with the transaction handled by a decorator.

HTMX

Routes that return HTML fragments instead of JSON.

Exceptions

A missing row raises NotFoundError and becomes a 404 on its own.

Step 1: Scaffold

Part 1 had no database, so the app has no models/ package, no migrations/ directory and no DATABASE_URL. Add them:
feather db init exists for exactly this case. In an app scaffolded with a database it fails, because migrations/ is already there.

Step 2: Run the prompt

The prompt
The pause before the migration is deliberate. Feather keeps migrations manual so you see what Alembic inferred from the models, which matters just as much when an assistant wrote them.

Step 3: Test it

  1. Click Add Column and give it a name. It appears without a reload.
  2. Type in the input at the bottom of a column and press Enter. The card appears, and the input clears.
  3. Hover a card and click the X. Confirm the dialog, and the card goes.
  4. Click the X on a column header. Confirm, and the column and its cards go together.
  5. Refresh the page. Everything is still there.
feather routes is worth a look here. You should see the page route at / plus four /htmx/ routes, which is everything this part added.

What the framework did

The mixins wrote your boilerplate. UUIDMixin gave every row a UUID primary key and TimestampMixin gave it created_at and updated_at. Neither appears in the model body. See Models. Deleting a column deleted its cards. That’s cascade="all, delete-orphan" on the relationship. There is no cleanup code in the service. The routes stayed thin. Each handler parses the request, calls a service and returns a template. A handler doing more than that is reported by feather check as the fat-route warning, which is why the logic ended up in services. A DELETE returns an empty string. HTMX swaps that emptiness over the element, which is how the row disappears. A POST returns the rendered partial for the thing it just made. Both patterns repeat for the rest of the series. See HTMX. The confirm dialog isn’t the browser’s. hx-confirm is backed by Feather’s confirm_modal component, so it’s styled and dark-mode aware. Native confirm() is a feather check error.

Next: drag and drop

The first part that needs real state in the browser.