Skip to main content
This is the part where the toy becomes an application. It’s also the longest, because authentication, cloud storage and the admin panel all arrive together, and each needs credentials before you can start. Set aside more time than the earlier parts, and get the credentials sorted first. Nothing below works without them. You’ll use: Google OAuth, @auth_required and @login_only, roles, the scaffolded admin panel, GCS file storage, and WeasyPrint for PDF export.

Credentials you need first

  1. Open APIs and Credentials.
  2. Create an OAuth 2.0 Client ID of type Web application.
  3. Add http://localhost:5173/auth/google/callback as an authorized redirect URI.
  4. Copy the client ID and secret.
  1. Create a bucket in Cloud Storage.
  2. Create a service account in IAM.
  3. Grant it the Storage Object Admin role.
  4. Create a JSON key and copy the entire contents.

Create the project

This part needs a different app type, so start fresh.
Four answers matter:
.env
GCS_CREDENTIALS_JSON has to be the whole JSON key on a single line. Copy it out of the downloaded file and strip the line breaks, or the app fails to authenticate against the bucket with an error that doesn’t obviously point back here.

Step 0: Clear out the demo files

tests/test_home.py goes with the route it tests. Leave it behind and feather test fails on a 404 the moment you delete routes/pages/home.py. Leave models/__init__.py alone for now. The Log, Account and AccountUser imports already in it have to stay.

Step 1: The models

A board now belongs to a user, and columns belong to a board.
models/kanban.py
models/column.py
models/card.py
models/__init__.py
Import order matters here. Alembic creates tables in the order the models are imported, and a foreign key can’t point at a table that doesn’t exist yet.

Step 2: The migration

Step 3: The services

A word about roles before the code. Feather ships four of them, admin, editor, moderator and user, and those are the only ones the scaffolded admin panel can assign from its dropdown. So this tutorial uses them as they are: editors and admins can change boards, everyone else gets a read-only view. New users arrive as user, and you promote someone from /admin/users.
Add it to ROLE_INHERITS in feather/auth/roles.py, then to the valid_roles list in services/admin_service.py, and to the <select> in templates/pages/admin/user_detail.html so the panel can assign it. The role column is a plain string, so no migration is needed.
services/kanban_service.py
services/column_service.py
services/card_service.py
services/__init__.py
Notice that every check goes through the relationship rather than trusting an ID from the request. A card’s owner is card.column.kanban.user_id, and that chain is what makes the authorization hold.

Step 4: The routes

routes/pages/dashboard.py
Why the pending page uses @login_only. @auth_required blocks a suspended user with a 403, which would make the pending page unreachable by exactly the people who need it. @login_only checks only that there’s an authenticated session, without looking at active or role.
routes/pages/board.py

The API routes

Move, export and attachments all live here.
routes/api/board.py
The upload never touches a Google SDK. get_storage() returns whichever backend STORAGE_BACKEND names, so the same code works against the local filesystem if you switch it.

Steps 5 and 6: Templates and CSS

This part adds seven templates and a sizeable stylesheet: login, pending, the dashboard grid, the board, and partials for the board card, the column, the card, the PDF viewer modal and the upload modal. They’re mostly presentation, and reproducing four hundred lines of CSS here wouldn’t teach you anything the earlier parts didn’t. Copy them from Steps 5 and 6 of the source tutorial. The one pattern worth calling out is how the templates gate on role:
The service already refuses the action. Hiding the button as well means a read-only user never sees a control that would fail.

Step 7: The island

Unchanged from part 3, other than living in a new project.
static/islands/kanban-board.js

Step 8: The export button

The route already exists, so this is a link in the board header.
templates/pages/board.html

Steps 9 and 10: OAuth and testing

Confirm your Google credentials list http://localhost:5173/auth/google/callback as a redirect URI, then:
Work through it in this order:
  1. Sign in with Google.
  2. Create a board from the dashboard.
  3. Add columns and cards.
  4. Go back and create a second board.
  5. Open /admin/ as an admin.
  6. Attach a PDF to a card, then open it in the viewer.
  7. Export the board.
Then check the roles behave: an admin gets everything including /admin/, an editor can create and edit, and a plain user sees the boards with no create, edit or delete controls.

Prompt Claude

Run this in two passes. The first gets you a working app behind sign-in, the second adds files and export.

Checkpoint

  • Google sign-in works, and a new account lands on the pending page until approved
  • The dashboard shows your boards in a grid
  • A board opens with columns, cards and working drag-and-drop
  • /admin/ is reachable as an admin and refused otherwise
  • A PDF attaches to a card and opens in the viewer modal
  • Exporting a board produces a PDF
  • A plain user sees no create or delete controls
  • feather check passes

What you learned

  • Google OAuth end to end, including the approval gate
  • @auth_required versus @login_only, and why the pending page needs the second
  • Reading current_user and its role
  • Checking ownership through relationships rather than request parameters
  • The scaffolded admin panel, and who can reach it
  • File storage through get_storage() rather than a cloud SDK
  • PDF generation with WeasyPrint
  • Gating UI controls on role so they match what the services allow

Next: turn it into a SaaS

Tenant isolation, platform admin, and queries that can’t leak across organizations.