Agent Instructions¶
The page below is included verbatim from template-app/AGENT_INSTRUCTIONS.md so the docs site can never drift from the actual instructions the agent reads.
AGENT_INSTRUCTIONS.md¶
You are an AI agent working inside a SCALR app. This file is your operating manual for two tasks:
- Modifying this app (most common — adding endpoints, UI, data models, migrations).
- Scaffolding a new app from the template (rarer — covered at the bottom).
Read all of it before touching anything. The architecture is fixed; your job is to add domain logic on top of it without breaking the wiring.
Stack — never change without an ADR¶
The locked stack from ADR 0002. If you find yourself wanting to deviate, stop and ask.
| Layer | Tech |
|---|---|
| Backend | FastAPI (Python 3.12), SQLAlchemy 2.x, Alembic |
| Frontend | React 18 + Vite + TypeScript, Tailwind CSS |
| Database | Postgres 16 (one per app, isolated) |
| Auth | Authentik (OIDC) — see ADR 0003 |
| Routing | Traefik v3 — see ADR 0007 |
| Workspace deps | @scalr/auth-client, @scalr/ui (TS), scalr_auth_client_py (Py) |
If a problem with one of these comes up, write a new ADR proposing the change — don't quietly swap the tech.
How this app connects to the platform¶
Network & routing (gateway-mode)¶
Browser → http://<slug>.localhost ─┐
├─→ Traefik (services/gateway/)
Browser → http://<slug>.localhost/api/* ─┘ │
├─→ <slug>-frontend (Vite dev :5173)
└─→ <slug>-backend (uvicorn :8000)
│
└─→ <slug>-db (Postgres :5432)
- The frontend talks to the backend at the same origin (
/api). No CORS in this mode. - Apps join the shared
scalr-edgeDocker network so Traefik can reach them. - Each app's DB sits on its own private network — physically unreachable from other apps. See the per-app
docker-compose.ymlfor the network wiring. - Authentik is not behind the gateway in v0. It runs at
http://localhost:9000(host) andhttp://host.docker.internal:9000(from inside containers).
Auth wiring¶
Backend — every protected route uses Depends(require_user):
# backend/src/routes/<feature>.py
from fastapi import APIRouter, Depends
from ..deps import User, require_user
router = APIRouter()
@router.get("/things")
def list_things(user: User = Depends(require_user)) -> list[Thing]:
...
require_user is re-exported from backend/src/deps.py, which wraps scalr_auth_client_py.deps.require_user. It validates JWTs against Authentik's JWKS (configured via AUTH_JWKS_URL). Don't write your own JWT validation.
Frontend — every API call uses useAuthFetch:
import { useAuthFetch, useUser } from "@scalr/auth-client";
const authFetch = useAuthFetch();
const response = await authFetch(`${API_BASE}/things`);
AuthProvider (in frontend/src/main.tsx) auto-redirects to Authentik on first load if there's no session, and the redirect is silent if the user is already signed into another SCALR app via SSO. Don't replace this with a custom hook.
Authentik side — each app has its own OIDC Provider (Client ID = slug, Client type = Public) and an Application bound to it, plus a binding to the scalr-users group. All of this is auto-provisioned by the per-app blueprint at services/auth/blueprints/apps/<slug>.yaml (the scaffold script writes one for every new app). Authentik's worker reconciles roughly every minute; restart the worker to apply immediately. Manual UI fallback walkthrough still exists at docs/content/guides/configuring-authentik-oidc.md for diagnosing failures or making one-off tweaks.
Database¶
Each app has its own Postgres instance:
- Container:
<slug>-db, imagepostgres:16-alpine. - Connection: backend reaches it as
db:5432over the per-app network. - Credentials:
POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DBfrom env (see Env section). The connection string is built inbackend/src/settings.py. - Volume:
db-data(per-compose-project namespaced — totally separate from other apps). - Migrations: Alembic. The backend's
entrypoint.shrunsalembic upgrade headon boot, so new migrations apply automatically.
Never share data across apps via SQL. Cross-app communication goes over HTTP between backends.
Env files (see ADR 0008)¶
Two files merged at compose time:
| File | Where | Committed? | Contents |
|---|---|---|---|
.env.shared |
repo root | no (gitignored) | the only secrets — POSTGRES_PASSWORD, SCALR_TEST_USER_PASSWORD |
.env.app |
per-app | yes | structural config — slug, ports, OIDC IDs, DB name, issuer URLs |
make up (and make down, down-v) pass both via --env-file flags. Later wins, so .env.app can override a shared value if it ever needs to.
Never put secrets in .env.app. Never copy .env.shared into the app dir. There's no .env and no .env.example — those are gone.
Workspace packages¶
Imports of @scalr/auth-client, @scalr/ui, and scalr_auth_client_py resolve to the source under <repo>/packages/*. The TypeScript packages' src/ dirs are bind-mounted into the frontend container, so edits hot-reload without a rebuild. The Python package is pip install -e'd at backend image build, so edits to it do require docker compose build backend.
You can edit App.tsx and see the change instantly. You can edit @scalr/auth-client/src/provider.tsx and also see the change instantly. But you can't edit scalr_auth_client_py/deps.py without rebuilding the backend.
Procedure A — modifying this app¶
This is the common case. The user wants you to add a feature, change a model, fix a bug, etc.
1. Pre-flight¶
- Re-read this file in full. Especially the connections section above.
- Check the platform is running:
docker psshould showscalr-gateway,scalr-auth-server, and<slug>-{frontend,backend,db}. - If something's not running:
cd <repo-root> && make up. If auth specifically:cd services/auth && docker compose up -d.
2. Common modifications — where to put what¶
| Change | File(s) |
|---|---|
| New protected API endpoint | backend/src/routes/<feature>.py (new file) → register in backend/src/main.py with app.include_router(<feature>_routes.router, prefix="/api") |
| Request/response schema | backend/src/schemas.py (Pydantic models) |
| New DB table or column | backend/src/models.py → cd backend && alembic revision --autogenerate -m "<msg>" → review the generated migration before committing |
| New UI page or section | frontend/src/App.tsx, or break out into frontend/src/components/<Foo>.tsx |
| Use a new shared UI primitive | Add to packages/ui/src/, export from packages/ui/src/index.ts (lives across all apps) |
| Manifest metadata (description, icon, color, category) | app.manifest.yml |
| Docs about the app | README.md |
3. Boot and verify¶
Most changes hot-reload — Vite watches the frontend, uvicorn --reload watches the backend. You should usually see your change without restarting anything.
Cases where you DO need to restart or rebuild:
- Backend dependency changes (
pyproject.toml):docker compose --env-file ../../../.env.shared --env-file ./.env.app build backend && docker compose ... up -d backend. (From the repo root:make upwith the relevant subproject does this.) - Frontend dependency changes (
package.json): rebuild the frontend image — same shape. - Alembic migrations: usually auto-applied by
entrypoint.shon container start. If the backend is already running,docker compose exec backend alembic upgrade headis enough. - Compose changes:
docker compose up -dagain to recreate. - Env changes: same — recreate the affected container.
4. Test¶
Browser test:
- Hit
http://<slug>.localhost(or open via the portal athttp://portal.localhost). - Sign in if asked. SSO will be silent if you're already signed into another SCALR app.
- Exercise the feature.
Smoke test:
./scripts/smoke-test.sh
All five checks must pass before declaring done. If you've added new endpoints, consider extending the smoke test (its checklist is in ADR 0006).
Procedure B — scaffolding a new app¶
Only relevant when the user explicitly asks for a new app. Do not scaffold a new app to add a feature to an existing one.
# Interactive (recommended)
python3 infra/scripts/new-app.py
# Or non-interactive
make new-app NAME="<Display Name>" SLUG=<kebab-slug> KIND=product|client|internal|platform \
DESCRIPTION="<one sentence>"
The script:
- Validates inputs (slug is kebab-case, unique).
- Copies
template-app/toapps/<kind>s/<slug>/(orclients/,services/). - Allocates a port range from the next available slot.
- Substitutes slug-derived values throughout.
- Fixes depth-relative paths (compose context, package live-mounts, workspace
file:deps). - Validates the manifest.
- Appends the new path to
infra/registered-apps.txtsomake upincludes it. - Writes
services/auth/blueprints/apps/<slug>.yaml— the OIDC Provider, Application, andscalr-usersgroup binding. Authentik's worker reconciles it on the next pass (~60s).
The script does not:
- Boot the app (the interactive new-app.py optionally does, the bash script doesn't).
- Run the new app's smoke test.
- Wait for the Authentik blueprint to apply (it runs asynchronously in Authentik's worker).
You then need to:
- Make sure
<repo>/.env.sharedexists withPOSTGRES_PASSWORDandSCALR_TEST_USER_PASSWORDset. (new-app.pychecks for this and bails if it's missing.) - (Optional) Force the OIDC blueprint to apply immediately:
cd services/auth && docker compose restart worker. Otherwise wait ~60s for the next reconcile cycle. make upfrom the repo root, ordocker compose --env-file ../../../.env.shared --env-file ./.env.app up -d --buildfrom the new app's dir.- Open
http://<slug>.localhost, sign in, see the template's "Hello," page. - Now apply Procedure A to add the actual feature the user wanted.
If sign-in fails with "no provider for client_id=...", the blueprint hasn't applied yet — restart the worker (step 2) or check the worker logs for blueprint errors:
cd services/auth && docker compose logs worker | grep -i blueprint.
Hard rules¶
These are non-negotiable. Each one exists because of a specific failure mode that already cost time once.
- Don't change the stack. No swapping React for Vue, FastAPI for Express, Postgres for SQLite, Authentik for Auth0. The template is the contract.
- Don't disable auth. Every backend route except
/healthusesDepends(require_user). If you're tempted to add a public route, it's almost always actually protected — the user just isn't signed in yet. - Don't share databases between apps. Each app has its own Postgres. Cross-app data goes through HTTP APIs, not SQL joins.
- Don't write your own JWT validation. Use
scalr_auth_client_py.require_user. If you think you have a reason to roll your own, you don't. - Don't skip Alembic. All schema changes go through
alembic revision --autogenerate. Direct DDL in route code is forbidden. - Don't commit
.env.shared. It's gitignored; keep it that way. Don't paste secrets into.env.appeither — that file is committed. - Don't allocate ports manually. The scaffold script does this. If you're picking port numbers by hand, stop — re-run
new-app.py. - Don't modify
app.manifest.yml'sidorslugafter creation. Other manifests, audit logs, and entitlements key off these. - Don't bypass the gateway in URLs you give to users. Use
http://<slug>.localhost(dev) orhttps://<slug>.scalr.com(prod), notlocalhost:<port>. Direct-port URLs bypass Traefik and break/apisame-origin routing. - Don't edit files under
packages/casually. Those changes affect every app. If you must, understand which files hot-reload (TSsrc/) vs which require a rebuild (TSpackage.json, all of Python).
Definition of done¶
You can tell the user the work is finished only when ALL of these are true:
- The change compiles and the affected containers are healthy (
docker compose psshows them up). - For backend changes: hitting the new/changed endpoint with a valid token returns the expected response. Without a token: 401.
- For frontend changes: the affected page renders correctly in the browser at
http://<slug>.localhost, signed in as the test user. - For data-model changes: there is exactly one new Alembic migration file under
backend/alembic/versions/, it applies cleanly, and you've reviewed the generated SQL. ./scripts/smoke-test.shexits 0.app.manifest.ymlstill validates:make validate-manifests.- Anything you've changed about the app's purpose is reflected in
README.mdand the manifest'sdescription.
Failure recovery¶
Common failure modes, in rough order of frequency:
Browser shows a blank white screen. The auth flow needs a configured OIDC client in Authentik for this slug. Check Authentik admin (http://localhost:9000) for a Provider + Application with Client ID = this app's slug. If absent, follow the OIDC guide.
Browser shows "Error from API: Unexpected token '<', '<!doctype …'". The frontend hit a URL that returned HTML instead of JSON — usually because someone gave you a localhost:<port> URL instead of <slug>.localhost. Hit the gateway URL instead.
Backend returns 401 with Missing bearer token. Expected when unauthenticated. If you ARE signed in and still see this: token expired, or the token's aud claim doesn't match AUTH_AUDIENCE. Re-sign-in usually fixes it.
Backend returns 500 with database errors after a model change. Migration didn't apply. Check docker compose logs backend for the alembic output. Most common cause: you forgot to run alembic revision --autogenerate and only edited the model.
Port collision (address already in use) when starting an app. Another container already grabbed the host port. docker ps to find it, stop it, retry.
Compose says POSTGRES_PASSWORD is required. .env.shared doesn't exist or wasn't loaded. From the repo root: [ -f .env.shared ] || cp .env.shared.example .env.shared, then edit it, then retry. The Makefile target check-shared-env runs this preflight.
Compose says variable interpolation failed for some ${X}. Either .env.app or .env.shared is missing the key. Look at the compose file's environment: block to see which file should provide it.
If you're stuck, gather the failing logs (docker compose logs <service>) and tell the user — don't silently leave the app broken or revert work without explanation.