Skip to main content
The board from part 1 forgets everything when you reload. Here you give it a database and make the buttons work, and you’ll do it without writing any frontend JavaScript beyond one click handler. That’s the point worth noticing. Creating and deleting cards is exactly the kind of interaction people reach for React to handle. HTMX does it by asking the server for a fragment of HTML and swapping it into the page. You’ll use: SQLAlchemy models with mixins, Alembic migrations, services, dependency injection, HTMX attributes, and partial templates.

Prerequisites

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

Step 1: The models

You’re typing these out because seeing them is the point. In your own projects the CLI writes the skeleton:
That gives you a model with UUIDMixin and TimestampMixin already applied. Add --ordered for OrderingMixin, which part 3 uses, or --soft-delete for SoftDeleteMixin. Run feather generate --help for the rest.
models/column.py
models/card.py
models/__init__.py

Step 2: The migration

Read the generated file before you apply it. Feather keeps this step manual so you see what Alembic inferred from your models.

Step 3: The services

services/column_service.py
services/card_service.py
services/__init__.py
The Service base class hands you self.db and self.save(). Raising NotFoundError from get_by_id means a missing ID becomes a 404 without a single try block in your routes.

Step 4: The board route

routes/pages/home.py
Three patterns repeat across every HTMX route you’ll write:
  • A POST returns the rendered partial for the thing it just created.
  • A DELETE returns an empty string, because the element is going away.
  • Routes live under /htmx/ so it’s obvious at a glance which ones return fragments.

Step 5: The partials

templates/partials/column.html
templates/partials/card.html

The HTMX attributes

Step 6: The board template

templates/pages/board.html
static/js/board.js
Ordinary JavaScript files like board.js are not Vite entry points. Only vendor, styles and islands/* are built by Vite, which is why the template loads this one from the dev server in debug and through url_for in production.
What changed from part 1: columns now come from a partial rather than inline markup, the Add Column button uses Feather’s styled prompt modal instead of window.prompt, and the JavaScript lives in its own file. Inline scripts are a feather check error.

Step 7: The new CSS

static/css/app.css

Step 8: Test it

  1. Click Add Column and give it a name.
  2. Type in the input at the bottom of a column and press Enter.
  3. Hover a card and click the X. Confirm the dialog.
  4. Click the X on a column header. Confirm that too.
  5. Refresh the page. Everything is still there.

Prompt Claude

Asking it to pause at the migration is deliberate. Feather keeps migrations manual so you see what Alembic inferred from your models before it touches the database, and that’s worth preserving when an assistant wrote the models.

Checkpoint

  • Columns and cards are stored in SQLite
  • Creating and deleting either one happens without a page reload
  • Deletes ask for confirmation first
  • Everything survives a refresh
  • feather check passes
Files you created or changed

What you learned

  • Models built from UUIDMixin and TimestampMixin
  • Generating and applying migrations
  • Services as the home for business logic
  • @inject for getting a service into a handler
  • HTMX attributes for create and delete
  • Partial templates as HTMX responses
  • window.showPrompt() instead of the native dialog

Next: drag and drop

Add ordering to the models and a JavaScript island to move cards between columns.