Skip to content

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-edge Docker 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.yml for the network wiring.
  • Authentik is not behind the gateway in v0. It runs at http://localhost:9000 (host) and http://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, image postgres:16-alpine.
  • Connection: backend reaches it as db:5432 over the per-app network.
  • Credentials: POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB from env (see Env section). The connection string is built in backend/src/settings.py.
  • Volume: db-data (per-compose-project namespaced — totally separate from other apps).
  • Migrations: Alembic. The backend's entrypoint.sh runs alembic upgrade head on 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 ps should show scalr-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.pycd 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 up with the relevant subproject does this.)
  • Frontend dependency changes (package.json): rebuild the frontend image — same shape.
  • Alembic migrations: usually auto-applied by entrypoint.sh on container start. If the backend is already running, docker compose exec backend alembic upgrade head is enough.
  • Compose changes: docker compose up -d again to recreate.
  • Env changes: same — recreate the affected container.

4. Test

Browser test:

  1. Hit http://<slug>.localhost (or open via the portal at http://portal.localhost).
  2. Sign in if asked. SSO will be silent if you're already signed into another SCALR app.
  3. 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:

  1. Validates inputs (slug is kebab-case, unique).
  2. Copies template-app/ to apps/<kind>s/<slug>/ (or clients/, services/).
  3. Allocates a port range from the next available slot.
  4. Substitutes slug-derived values throughout.
  5. Fixes depth-relative paths (compose context, package live-mounts, workspace file: deps).
  6. Validates the manifest.
  7. Appends the new path to infra/registered-apps.txt so make up includes it.
  8. Writes services/auth/blueprints/apps/<slug>.yaml — the OIDC Provider, Application, and scalr-users group 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:

  1. Make sure <repo>/.env.shared exists with POSTGRES_PASSWORD and SCALR_TEST_USER_PASSWORD set. (new-app.py checks for this and bails if it's missing.)
  2. (Optional) Force the OIDC blueprint to apply immediately: cd services/auth && docker compose restart worker. Otherwise wait ~60s for the next reconcile cycle.
  3. make up from the repo root, or docker compose --env-file ../../../.env.shared --env-file ./.env.app up -d --build from the new app's dir.
  4. Open http://<slug>.localhost, sign in, see the template's "Hello, " page.
  5. 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.

  1. Don't change the stack. No swapping React for Vue, FastAPI for Express, Postgres for SQLite, Authentik for Auth0. The template is the contract.
  2. Don't disable auth. Every backend route except /health uses Depends(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.
  3. Don't share databases between apps. Each app has its own Postgres. Cross-app data goes through HTTP APIs, not SQL joins.
  4. 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.
  5. Don't skip Alembic. All schema changes go through alembic revision --autogenerate. Direct DDL in route code is forbidden.
  6. Don't commit .env.shared. It's gitignored; keep it that way. Don't paste secrets into .env.app either — that file is committed.
  7. Don't allocate ports manually. The scaffold script does this. If you're picking port numbers by hand, stop — re-run new-app.py.
  8. Don't modify app.manifest.yml's id or slug after creation. Other manifests, audit logs, and entitlements key off these.
  9. Don't bypass the gateway in URLs you give to users. Use http://<slug>.localhost (dev) or https://<slug>.scalr.com (prod), not localhost:<port>. Direct-port URLs bypass Traefik and break /api same-origin routing.
  10. Don't edit files under packages/ casually. Those changes affect every app. If you must, understand which files hot-reload (TS src/) vs which require a rebuild (TS package.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 ps shows 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.sh exits 0.
  • app.manifest.yml still validates: make validate-manifests.
  • Anything you've changed about the app's purpose is reflected in README.md and the manifest's description.

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.