Skip to content

feat: cookie auth, generated client with Pinia Colada, hybrid SSR and agent tooling - #4

Merged
bubaley merged 22 commits into
mainfrom
modernize/stage-5
Oct 22, 2026
Merged

bubaley merged 22 commits into
mainfrom
modernize/stage-5

Conversation

@bubaley

@bubaley bubaley commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Summary

Stages 5 and 7 of the template modernization (plan archived in docs/archive/roadmap-2026.md; open items in docs/follow-ups.md).

  • Stage 5a — API contract and cookie auth (backend): login/refresh/logout/logout-all/me with HttpOnly cookies only (access_token path / 15 min, refresh_token path /api/v1/auth 30 days); CSRF Origin check for cookie-authenticated unsafe requests; websocket auth from the cookie; stable OpenAPI (<tag>_<action> operationIds, security schemes, documented error envelope, make openapi → frontend/openapi.json with a staleness test); members are added to the organization named in the body. Temporary split of ronin's combined PUT/PATCH route until the fixed fastapi-ronin is released.
  • Stage 5b — frontend data layer: @hey-api/openapi-ts client (make gen-client, gen:api:check in pnpm check), Pinia Colada with typed resource descriptors behind AppListTemplate/AppDetailTemplate, useSession() with single-flight refresh on 401, Nitro auth proxy and the repo toolkit removed, hybrid rendering (routeRules: app routes client-only, / is an SSR public page with SEO/OG), same-origin dev proxy for /api/v1 and /ws.
  • Stage 7 — agent tooling and forks: auto-discovery of domain models and routers; make new-domain scaffolder (backend domain + tests, frontend feature, pages, data-driven settings nav); make init bootstrap for new products (rename, fresh secrets, version reset, .template-version); docs/template-core.md vs docs/product.md; docs/template-ownership.md + /sync-template; .claude/ skills (new-domain, verify, design-system, add-shadcn-component, sync-template, release), review subagents and hooks (block migrations unless ALLOW_MIGRATIONS=1, format edited files); VS Code settings for Oxc/Ruff.
  • Fix found during manual review: the readiness probe could cancel the first query while it lazily built the asyncpg pool, leaving every later query hung. The pool is now built at startup and the probe query is shielded. Dev Makefile targets run through uv.

Breaking changes for forks

  • Auth responses no longer contain tokens; clients must use cookies (browser) or API tokens (Authorization: Token).
  • Home moved from / to /home, setup to /home/setup.
  • POST /organization-users requires organization_id in the body; user-tokens create returns 201; logout returns 204.

Test plan

  • make check against PostgreSQL: 186 backend tests, 65 tooling tests, frontend oxfmt/oxlint/ESLint/typecheck/67 vitest/knip/architecture guard/generated client up to date
  • pnpm build
  • curl end-to-end through the dev proxy: login → me → list → create (403 without Origin) → refresh → logout → 401
  • Browser: login, organizations, users at 1024 light and 768 dark; earlier pass by the stage 5b agent at 1440/390
  • Scaffolder generated a domain end to end (tests, gen-client, check, build) and was cleaned up; make init verified on a scratch clone
  • CI on this PR

🤖 Generated with Claude Code

bubaley and others added 22 commits October 22, 2026 21:38
…ests

Login, refresh, logout and logout-all set and clear the HttpOnly
`access_token` (path /) and `refresh_token` (path /api/v1/auth) cookies;
no token is ever returned in a body. Login returns the user, their
organizations and the cookie expiry; refresh reads only the refresh
cookie and returns the expiry; logout and logout-all answer 204;
`GET /auth/me` returns the user with their organizations.

Credentials resolve in order: access cookie, `Authorization: Token`,
`Authorization: Bearer` (kept for tests and server-side scripts).
Unsafe requests that rely on the auth cookies need a trusted Origin
(or Referer): the API host or CORS_ORIGINS, else 403. Header-authenticated
and safe requests are exempt, so SSR can forward the Cookie header on GETs.
Websockets share the trusted-origin check. Access tokens now live 15 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`POST /organization-users` required the target organization to come from
the `Organization-Key` header, so adding a member from an organization's
page used the active organization instead of the one being viewed. The
body now carries a required `organization_id`; the actor must belong to
it (or be a superuser) and an unknown organization is 404. The read
schema exposes `organization_id` as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- operationIds `<tag>_<route name>` (organizations_list, auth_login, ...)
- every API operation documents the error envelope (ErrorResponse) for
  400/401/403/404/409/422/429/500
- security schemes: access cookie, `Authorization: Token`, Bearer JWT;
  public auth routes declare none
- `Organization-Key` declared as an optional header on viewsets that use
  it (organization-scoped models, organization assigned from the header)
- list responses named `<ReadSchema>Page`; user token create answers 201
- temporary split of ronin's combined PUT/PATCH route into `update` and
  `partial_update` (fixed upstream on fix/split-put-patch-operations)
- `manage.py openapi [--output]` dumps the schema offline; `make openapi`
  writes frontend/openapi.json; a test keeps the committed file current

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hey-api/openapi-ts (pinned) writes types, SDK functions and the fetch client
into app/shared/api/generated. `pnpm gen:api` regenerates and formats it,
`pnpm gen:api:check` fails when the committed output is stale and runs in
`pnpm check`; `make gen-client` chains `make openapi` and `gen:api`.
Generated code is excluded from oxlint, knip and the architecture guard but
still typechecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a layer

The browser talks to FastAPI on its own origin (Caddy in production, a
nuxt dev proxy for /api/v1 and /ws in development), so the HttpOnly session
cookies and the CSRF Origin check work unchanged. plugins/api.ts builds the
generated fetch client per app instance: Organization-Key on every request,
the request cookie forwarded to NUXT_API_INTERNAL_URL during SSR, and in the
browser a single-flight POST /auth/refresh plus one replay on 401; a failed
refresh signs out and sends app routes to /auth?redirect=.

Features describe each entity once with defineResource (generated SDK
functions, form mapping typed against the request schemas, query-key root).
AppListTemplate, AppDetailTemplate, AppSelect and filter fields run on Pinia
Colada: infinite lists under [key, 'list', params], records under [key, id],
mutations invalidate the resource prefix. useSession replaces the base-auth
module and the Nitro auth proxy; useSetupStore restores the organization from
the session. App routes render client-only via routeRules. Members are added
to the organization being viewed (organization_id in the body).

Removed: modules/base-auth, server/, the repo toolkit, useRepo, codecs,
useRepoData, user-context plugin, NUXT_PUBLIC_BASE_URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/ is the template's public page placeholder: product name from app.config,
placeholder copy, a sign-in link (or a link into the app for a signed-in
visitor) and useSeoMeta title/description/og tags. It renders on the server
with the forwarded session cookie; the session is serialized in the payload,
so it hydrates without a mismatch with or without a session. The image smoke
test checks the og tags and no longer sets NUXT_PUBLIC_BASE_URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frontend-concept: generated client, resource descriptor, query keys and
invalidation, session and page meta guards, SSR rules for public pages,
realtime, adding a feature end to end. architecture: frontend tree and
request flow without base-auth and the Nitro proxy. README/deployment:
make gen-client, NUXT_API_INTERNAL_URL instead of NUXT_PUBLIC_BASE_URL.
product: public page, silent session renewal, sign out everywhere.
roadmap: stage 5 done, resolved follow-ups removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Models come from app/domains/*/models.py and routers from the `routers`
(or `router`) export of each views.py. Kernel domains keep their order so
openapi.json is unchanged; import errors raise DomainDiscoveryError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/scaffold/new_domain.py generates an organization-scoped CRUD entity
that mirrors the organization code: backend models/schemas/filters/views/
tests and the frontend resource, list/detail views, settings pages. The
settings drawer becomes data (app/navigation/settings.ts) so the scaffolder
can append its entry. Tooling tests run in check-backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/bootstrap/init.py renames the project across the greppable surface,
writes .env with fresh SECRET_KEY/ENCRYPTION_KEY, and on the first run resets
versions to 0.1.0, empties the changelog, resets docs/product.md to the
product skeleton and records the template version/commit in
.template-version. Idempotent; refuses a dirty tree unless --force.
Also adds make createsuperuser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.claude/settings.json wires a PreToolUse hook that blocks make m/mm/migrate/
make-migrations and tortoise migrate/makemigrations unless prefixed with
ALLOW_MIGRATIONS=1, and a PostToolUse hook that formats only the edited
backend/frontend file (ruff, vp fmt) without ever failing the edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/template-core.md lists the capabilities every product inherits;
docs/product.md becomes the product-owned spec (purpose, personas,
scenarios, non-goals) and wins on conflict. docs/template-ownership.md
says which paths forks own and how a template sync resolves conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Skills: new-domain, verify, design-system, add-shadcn-component,
sync-template, release — short checklists that link to the docs.
Subagents: domain-reviewer (backend rules) and ui-reviewer (browser pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion hook

Writing a README through a here-document that mentions `make m` was
blocked; here-document bodies are data, and newlines inside quotes do not
start a new command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…admap

README gains "Start a new product", "Adding a domain" and "Agent tooling";
architecture.md describes the .claude tooling, scaffolder and bootstrap.
Stage 7 is done, so the roadmap moves to docs/archive/roadmap-2026.md and
the open items to docs/follow-ups.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ness probe

A readiness probe timing out while the first query lazily created the
asyncpg pool left the pool half-built, so every later query hung until the
process restarted. Dev Makefile targets now run through uv, so they no
longer pick up a system Python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bubaley
bubaley merged commit f1b9560 into main Oct 22, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant