| name | create-app |
| description | Use when creating a new internal app or tool — an admin dashboard, an ops console, a back-office service — that needs the org's auth, structured logging, and deploy config wired correctly from the start. Triggers on "new internal app", "spin up a dashboard", "create a back-office tool", "scaffold an internal service", or any greenfield app that real employees will log into. Pre-wires default-deny auth, PII-scrubbing logs, and an env-pinned deploy so the security and observability basics aren't an afterthought. |
Create App
Overview
Internal apps leak in boring, repeatable ways: an auth check defaults to "allow"
and an unlisted route is wide open; a secret gets committed in a .env file;
logs ship raw email addresses and tokens to a system half the company can read;
a deploy meant for staging hits prod because the environment was never pinned.
This skill scaffolds a new internal app with those four basics already correct:
default-deny auth, secrets pulled from the secret store, PII-scrubbing structured
logging, and a deploy config that forces you to declare the environment. It's the
right starting point for an internal tool with an HTTP surface and human users —
NOT for a GPU model-serving service (use new-inference-service for that).
When to Use
Reach for this when:
- You're building a NEW internal app, dashboard, console, or back-office tool
that employees will authenticate into.
- You want auth, logging, and deploy wired the org's way without copying them
from another repo and inheriting its drift.
- You need the deploy to distinguish staging from prod and pull secrets from the
secret store rather than env files.
Do NOT use this for:
- A GPU-backed inference/model-serving service —
new-inference-service carries
the health-probe, GPU-request, and Temporal-drain spine that one needs.
- A public, unauthenticated marketing site — the default-deny auth here is built
for internal tools, not anonymous traffic.
- Adding a feature to an app that already exists — the scaffolding is for
greenfield only.
Running it
python .claude/skills/create-app/scripts/create_app.py \
--name ops-console \
--dest apps/ops-console
--name (lowercase-hyphenated) becomes the app id used in logging, auth audience,
and deploy labels. This creates:
apps/ops-console/
app/
main.py # app entrypoint, auth + logging wired in
auth.py # default-deny auth, audience = app name
logging_config.py # structured JSON logging with PII scrubbing
config/
auth.yaml # default-deny policy; secrets referenced, not inlined
deploy.yaml # env MUST be set (staging|prod); secrets from store
requirements.txt
Then: edit config/auth.yaml to add the routes/roles you actually allow (the
default denies everything), set the target environment in config/deploy.yaml,
and add your app logic in app/main.py.
What the scaffold guarantees
- Auth defaults to deny. Every route requires an authenticated, authorized
principal unless
config/auth.yaml explicitly allows it. Forgetting to list a
route fails closed (403), never open.
- Secrets come from the secret store.
deploy.yaml references secret names
the platform resolves at deploy time; there is no .env file and nowhere to
paste a credential into the repo.
- Logging scrubs PII before it leaves the process. The logging config runs a
scrubber over every record, redacting emails, tokens, and known PII keys so
they never reach the central log system.
- Deploy pins the environment.
deploy.yaml has a required environment
field with no default; the deploy tooling refuses to ship if it's unset, so a
staging build can't silently land in prod.
See assets/ for the templates the scaffold instantiates; each is real config,
not a placeholder.
Gotchas
ALWAYS treat these as real security/ops incidents — each has bitten an internal
app before.
- Auth must default to deny, and the allowlist is per-route. The dangerous
pattern is "authenticate the user, then allow everything" — one forgotten
@require_role and an admin route is open to every logged-in employee. The
scaffold's policy denies unlisted routes; you add explicit allow rules. Never
invert it to "allow by default, block sensitive routes" — you will forget one.
- Secrets come from the secret store, never an env file or the repo. A
committed
.env (or a secret pasted into deploy.yaml) ends up in git history
forever and in every developer's checkout. The deploy template references
secrets BY NAME and the platform injects them at runtime; if you find yourself
adding a literal credential to any file here, stop — that's the bug.
- The deploy config must pin the environment explicitly (staging vs prod).
An unset or defaulted environment is how a staging deploy reaches prod
databases.
deploy.yaml's environment field has NO default and the scaffold
leaves it blank on purpose so the deploy fails loudly until you choose. Don't
add a default to "make it convenient."
- Logs must scrub PII before they ship, not after. Redacting in the central
log system is too late — the raw email/token already left the process and was
stored. The scrubber runs inside
logging_config so the line is clean before
it hits stdout. If you add a new log field carrying user data, add it to the
scrubber's key list; don't assume downstream will catch it.
- Default-deny only protects routes that go through the auth layer. A route
mounted outside the middleware (a health check, a static handler, a debug
endpoint added "temporarily") bypasses the policy entirely. Keep every route
under the auth middleware except a deliberately tiny, reviewed set — and those
must expose nothing sensitive.
Files
scripts/create_app.py — CLI that validates --name, builds the app tree, and
instantiates the templates in assets/ with the name substituted. Referenced
by Running it above.
assets/main.py.tmpl — app entrypoint that installs logging first, then the
default-deny auth middleware, then routes.
assets/auth.py.tmpl — auth helper: verifies the principal and enforces the
per-route allow policy from auth.yaml, failing closed.
assets/auth.yaml.tmpl — default-deny policy file; routes/roles are allowlisted
explicitly and secrets are referenced by name, never inlined.
assets/logging_config.py.tmpl — structured JSON logging with a PII scrubber
that redacts emails, tokens, and known PII keys before emit.
assets/deploy.yaml.tmpl — deploy manifest with a required, defaultless
environment field and secrets sourced from the secret store.
assets/requirements.txt.tmpl — pinned framework + internal auth/logging libs.