---
type: plan
milestone: M0 — baseline
---

# M0 — baseline: sign-in, projects, access

The goal of M0 was deliberately narrow: prove the substrate before building the product.
Sign in as an admin, create a scouting project, put a client on it, and have that client
see exactly that one project and nothing else. Everything the product actually does —
crawling, the option library, the feedback game — waits until this holds.

## Locked decisions

**D1 — Authentication is BW Auth (Pattern B), not a Scout login.**
Scout stores no passwords. The request was "a login for Admin and User", and Pattern B
is how that is spelled on this server: the server owner creates a BW account, the person
signs in at `auth.bowden.works`, and Scout keeps its own session plus its own role. This
is not a workaround for the no-hand-rolled-auth rule; it is a better fit — one set of
credentials across every BW property, revocable centrally and instantly.

**D2 — The domain is PUBLIC; the app default-denies.**
A Pattern B app host cannot be gated (the gate claims `/login` and `/auth/*`), so the
whole-site login wall has to live inside the app. `app/middleware.py` denies every path
that is not explicitly public, so the only thing an anonymous visitor can fetch is the
SPA shell, `/api/me`, and `/api/meta`. The net effect matches an account-gated domain
without breaking sign-in.

**D3 — Two app-wide roles, no per-project roles.**
Admin manages everything; user sees only what they are added to. Per-project roles are a
schema change if they are ever wanted, not a flag added to `project_members` later.

**D4 — An admin can grant access before the person's first sign-in.**
`app_accounts` rows are pre-provisioned by resolving the BW username through
`/app/userinfo`. Without this, an admin could not add anyone until after they had logged
in — which would have made the "invite a client" flow impossible to run in the order
people actually work.

**D5 — No service worker.**
Recorded as a rule in `agents.md`. A PWA navigation fallback silently breaks Pattern B
sign-in, and the failure produces no backend logs at all. The baseline ships none so the
trap is not in the project to fall into.

## What M0 delivered

| Piece | Where |
|---|---|
| BW Auth flow (probe, login, callback, logout) | `main/app/routers/auth.py` |
| Default-deny middleware | `main/app/middleware.py` |
| Roles and project visibility | `main/app/services/authz.py` |
| Column-scoped login upsert | `main/app/services/accounts.py` |
| Projects + membership | `main/app/routers/projects.py` |
| People management | `main/app/routers/accounts.py` |
| SPA (sign-in, projects, project detail, people) | `main/frontend/src/` |

## Verified

Exercised against the live host, not just the test suite:

- `/api/meta` and `/api/me` answer anonymously; `/api/projects` and `/api/accounts`
  return `401 NOT_AUTHENTICATED`.
- `/auth/login` redirects to `auth.bowden.works/app-authorize` with PKCE S256;
  `/auth/probe` adds `prompt=none`.
- The SPA shell and its hashed assets serve; deep links return the shell.
- Alembic ran the baseline migration on first boot; the bootstrap admin roles seeded.
- `scout.bowden.works.conf` carries no `import id-auth`.

## Gates before M1

1. Rian signs in, creates a project, adds Darren, and Darren sees that project and no
   other. (Needs Darren's password set — see `.logs/handoff.md`.)
2. A pytest suite covering the authorization boundary specifically: a user must not reach
   a project they are not on, and must not reach any admin route.

## M1 — the product (not started)

Open questions to settle with rian before building, in rough order:

- **What gets crawled, and how is it captured?** Full-page screenshots, component crops,
  extracted palettes and type? The answer decides the storage model.
- **What is the client actually reacting to?** Whole homepages, or isolated attributes
  (colour, density, type personality)? Whole pages are easier to gather; attributes give
  a far more useful answer.
- **What is the game?** Pairwise this-or-that converges fast and feels good on a phone.
  Scoring and swiping are alternatives worth naming before one is assumed.
- **What is the output?** The artifact that goes into a design brief is the point of the
  whole tool, and it should be designed before the input is.
