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:
- 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_requestguard 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 checkgating the deploy - a test suite that needs no database
- two environments —
main→ int on merge,v*tag → production - secrets split between a committed
app.yamland a CI-rendered, gitignoredenv_secrets.yaml
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.
- Create an OAuth client at console.cloud.google.com/apis/credentials (type: Web application).
- 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/callbackfor local dev, and the int hostname if you want it there too. - Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETas repo secrets (or in.envlocally).
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.