agilentics / boiler

Read this rendered instead

# webapp-skeleton

A Flask + Postgres + App Engine starting point, extracted from a running
production app. It is deliberately small: the value here is the decisions, not
the code. Read [CLAUDE.md](CLAUDE.md) — that is the actual deliverable.

What is already wired:

- session-cookie auth with password sign-in, organisations, and roles → scopes
- "Sign in with Google" (OAuth 2.0 / OIDC), hidden until it is configured
- a `before_request` guard built as an allowlist, so new routes are protected by
  default
- CSRF on state-changing requests
- Postgres via Cloud SQL (unix socket in prod, TCP through the proxy in CI/local)
- Alembic, with `alembic check` gating the deploy
- a test suite that needs no database
- two environments — `main` → int on merge, `v*` tag → production
- secrets split between a committed `app.yaml` and a CI-rendered, gitignored
  `env_secrets.yaml`

## Start a new app

```powershell
# 1. copy, minus this repo's git history
Copy-Item -Recurse C:\development\boilerplates\webapp-skeleton C:\development\<name>
cd C:\development\<name>
git init

# 2. rename. `myapp` is the only placeholder token; it appears in app.yaml,
#    app-int.yaml, both workflows, settings.py, .env.example and the templates.
(Get-ChildItem -Recurse -File -Exclude *.pyc |
  Where-Object { $_.FullName -notmatch '\\(\.git|\.venv|__pycache__)\\' }) |
  ForEach-Object {
    $t = Get-Content $_.FullName -Raw
    if ($t -match 'myapp') { $t -replace 'myapp', '<name>' | Set-Content $_.FullName -Encoding utf8 }
  }

# 3. run it
python -m venv .venv; .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env      # edit DB_PASS
alembic upgrade head
python manage.py bootstrap --email you@example.com
python main.py                   # http://127.0.0.1:8080
```

## Everything you must change

`myapp` is the rename token above. These are the values it cannot cover — each
one is a real identifier that must exist before a deploy works:

| Where | What |
|---|---|
| `app.yaml`, `app-int.yaml` | `cloud_sql_instances` and `INSTANCE_UNIX_SOCKET` — the Cloud SQL connection name |
| `app.yaml` | `PUBLIC_BASE_URL` — the canonical public origin |
| `app-int.yaml` | `PUBLIC_BASE_URL` — the int hostname, once the service exists |
| both workflows | `CLOUD_SQL_INSTANCE`, `project_id` |
| GitHub repo secrets | `GCP_CREDENTIALS`, `DB_PASS`, `SECRET_KEY`, `INT_SECRET_KEY`, `SMTP_PASS`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` |

Every secret except the first four is optional: unset renders as an empty string,
which the app reads as "that feature is off". You can deploy before any of them
exist.

## Google sign-in

Off until configured — the button is hidden and `/auth/google/*` redirects to
`/login`, so nothing here blocks a first deploy.

1. Create an OAuth client at
   [console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials)
   (type: Web application).
2. Register every origin it must work at as a redirect URI — they are not
   interchangeable:
   `https://myapp.example.com/auth/google/callback`,
   `http://localhost:8080/auth/google/callback` for local dev, and the int
   hostname if you want it there too.
3. Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` as repo secrets (or in
   `.env` locally).

First sign-in creates the account **and** an organisation with that person as
owner. If that is not what you want — an invite-only product, say — replace the
`orgs_core.provision(...)` call in `app/core/oauth.py` with a lookup that refuses
an unknown address.

Generate a real `SECRET_KEY` with
`python -c "import secrets; print(secrets.token_urlsafe(48))"`. `main.py` refuses
to start with a placeholder when `ENV=prod`.

## GCP, once per project

```bash
gcloud app create --region=us-central1
gcloud sql databases create myapp      --instance=<instance>
gcloud sql databases create myapp_int  --instance=<instance>
gcloud sql users create myapp --instance=<instance> --password=<pw>
# the deploy service account needs roles/cloudsql.client and App Engine deploy roles
```

## What was deliberately left out

Not because it is unimportant — because it differs per app, and a template that
guesses is worse than one that says nothing. Ports from the source app if you
need them: API tokens for machine callers, email verification and password reset,
WebAuthn passkeys, Stripe billing, outbound webhooks, a staff/support console.

The one thing worth knowing: `auth.principal_for_user` is the seam every sign-in
path plugs into, and `session._begin_session` is where each of them ends. Google
sign-in uses both and is the worked example — a second provider is that file
again with different endpoints.

## Keeping this current

Boilerplate rots. When you fix something in a child app, ask whether the fix was
generic — if it was, it belongs here too. That question is the only defence
against this directory becoming a snapshot of how things were done once.