agilentics / boiler

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 — that is the actual deliverable.

What is already wired:

Start a new app

# 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 (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

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.