Level 2: Scaffold a Production App¶
Level 1 wired an agent in ten lines of Python. That is a great way to
learn the API, but it is not how you ship. symfonic init scaffolds a
complete, runnable application — FastAPI, Postgres, Alembic migrations,
auth, a web UI, Docker, a Makefile, and tests — wired to symfonic-core
and ready for docker compose up.
The scaffolder generates a runtime app you own, not a package you
import. You edit the generated files in place; there is no wheel to
build and no pip install -e .. See
Scaffolded projects are runtime apps, not Python packages
for the full rationale.
Prerequisites¶
- Python 3.11 or later
- pip (or uv, poetry, etc.)
- Docker + Docker Compose (optional, for the
docker-composecomponent)
Install¶
The init command renders Jinja2 templates, so install the init
extra (or any provider extra, which pulls it in):
pip install "symfonic-core[init]"
# or, with a provider extra you will use anyway:
pip install "symfonic-core[anthropic]"
This puts a symfonic executable on your PATH:
Commands:
chat Run a single query against the Symfonic agent.
migrate Migrate HMS data between storage backends (stub).
init Scaffold a new symfonic-powered project.
doctor Audit the current symfonic configuration for the v7.16 silent-fail traps.
migrateis a placeholder — it prints "not yet implemented" and exits.chatis a one-shot query runner covered at the end of this guide. The command you want here isinit.
symfonic init — Command Reference¶
| Argument / Option | Default | What it does |
|---|---|---|
PROJECT_NAME |
(none) | Positional directory-safe slug. Required unless --list-components or -i. |
--components |
all components | Comma-separated subset to include, e.g. fastapi,auth,webapp. |
--list-components |
off | Print the component catalog and exit. |
--llm-provider |
anthropic |
One of anthropic, openai, deepseek, google, ollama. |
--output-dir |
. |
Parent directory; the project is created at <output-dir>/<project_name>. |
--interactive, -i |
off | Prompt for project name, provider, and per-component include/exclude. |
The project name must start with a letter and contain only letters,
digits, dashes, or underscores. If <output-dir>/<project_name> already
exists, the scaffolder refuses to overwrite it and exits with an error —
remove it or pick a different name.
Interactive mode¶
Prompts for the project name, the default LLM provider, and (if you opt
in) walks every component asking whether to include it. If you decline
all of them it falls back to fastapi only.
List the components¶
Components¶
Every component maps to a slice of the generated tree. fastapi is the
kernel — it is always included and supplies main.py, config.py,
db.py, requirements.txt, and .env.example. The rest are optional.
| Component | What it adds |
|---|---|
fastapi |
FastAPI kernel — main.py, config.py, db.py, requirements.txt, .env.example (always included). |
webapp |
Jinja2 + HTMX web frontend — webapp/ tree, static assets, HTML templates. |
docker-compose |
Container stack — Dockerfile, docker-compose.yml, Makefile docker targets. |
auth |
Authentication — app/auth/, alembic 001 users migration, login/signup pages, seed_admin. |
chats |
Chat history — app/chats/, alembic 002 chats migration, chat UI (onboarding + sidebar + stream JS). |
admin |
Admin panel — app/admin/, admin.html template. |
orgs |
Organisations — app/orgs/, orgs.html template, invitation flow. |
onboarding |
User onboarding — app/onboarding/ router, onboarding form, auto-redirect from chat until completed (requires auth). |
store-starter |
Store Copilot domain plugin — app/domains/store/ with 8 sales/inventory tools (top_sellers, sales_trend, sales_chart, revenue_summary, product_lookup, list_low_stock, register_product, update_stock) + guardrails and an Insights dashboard. Mutually exclusive with the generic starter domain. |
A few rules the scaffolder enforces for you:
onboardingrequiresauth. Onboarding keys tenants off the logged-in user, so if you omitauththe onboarding files are dropped even if you list them.store-starterreplaces the generic starter domain. When it is active,app/domains/starter/is omitted andapp/domains/store/ships instead — you get exactly one domain package.- Web templates are gated on
webapp. Feature pages like the admin panel ship only when both their feature andwebappare selected, so you never get dead nav links.
Common recipes¶
# Minimal API-only agent (no web UI, no auth, no Docker)
symfonic init my-agent --components fastapi
# API + auth + web UI, run locally without Docker
symfonic init my-agent --components fastapi,auth,webapp
# Everything (the default) — full multi-tenant SaaS skeleton
symfonic init my-agent
# Store Copilot starter instead of the generic domain
symfonic init my-store --components fastapi,webapp,docker-compose,auth,store-starter
Walkthrough¶
Scaffold a focused API-plus-auth app:
The scaffolder prints the path it created and a tailored next-steps
block (note it omits the Docker line because we did not include
docker-compose):
Symfonic project 'demo-app' scaffolded at:
.../demo-app
Next steps:
cd demo-app
cp .env.example .env
# edit .env and set ANTHROPIC_API_KEY
pip install -r requirements.txt
uvicorn app.main:app --reload
What you get¶
The fastapi,auth selection generates:
demo-app/
.env.example secrets + provider keys (JWT_SECRET pre-generated)
CUSTOMIZE.md the 5-layer HMS customization map
Makefile make migrate / seed-admin / seed-memory / dev / test
README.md stack overview + production checklist
alembic.ini
alembic/
env.py
versions/
001_initial.py users table
003_token_usage.py
004_audit_log.py
app/
main.py FastAPI app factory, lifespan, router wiring
config.py pydantic-settings (.env -> typed config)
db.py async SQLAlchemy engine + session factory
auth/ signup, login, JWT, get_current_user dependency
billing/ token-usage / budget routes (auth-gated)
common/rate_limit.py slowapi limiter for the auth endpoints
seed_admin.py create the initial admin from .env
audit_model.py
worker.py background consolidation loop entry point
setup/ app wiring: agent, middleware, routers, pages, audit, budget
domains/starter/ <-- YOUR AGENT LIVES HERE
plugin.py identity + personality (checkpoint 1)
tools.py tool list (checkpoint 2)
rules.md procedural rules (checkpoint 3)
seed_dev_memory.py starter semantic nodes
tests/
conftest.py
test_smoke.py
test_tenant_auth.py
test_budget_persistence.py
test_metacognition_smoke.py
test_streaming_smoke.py
Add webapp and you also get webapp/templates/ (Jinja2 + HTMX) and
webapp/static/css|js/ including custom.css (checkpoint 4). Add
docker-compose and you get Dockerfile, docker-compose.yml, and
the docker targets in the Makefile.
The generated requirements.txt pins symfonic-core with a tight range,
e.g.:
How to run it¶
The generated .env.example carries a pre-generated JWT_SECRET and a
slot for each provider key. Copy it and fill in the one key that matches
your provider:
cd demo-app
cp .env.example .env
# edit .env: set ANTHROPIC_API_KEY (LLM_PROVIDER=anthropic by default)
Then follow the generated Makefile's first-boot order. Without the
docker-compose component (local venv):
pip install -r requirements.txt
make migrate # alembic upgrade head — required before first boot
make seed-admin # create the initial admin user from .env (auth only)
make seed-memory # seed the starter memory nodes (needs Postgres)
make dev # uvicorn app.main:app --reload on :8000
With the docker-compose component, the Makefile targets run inside
the container and Postgres/Redis come up with the stack:
make up # docker compose up --build (postgres, redis, app, worker)
make migrate # runs alembic inside the app container
make seed-admin
make seed-memory
Then open http://localhost:8000.
Schema note: the app does not call
Base.metadata.create_all(). Alembic migrations are the single source of truth — always runmake migratebefore the first boot and on every deploy. The generatedREADME.mdcarries a full production checklist (env vars, migrations, rate limiting, secrets, scaling, observability).
Multi-tenant note¶
The scaffolded app is a multi-tenant FastAPI service — every request is scoped to a tenant. When you wire per-tenant LLM credentials, do not bind an API key onto a shared provider instance: a cached provider would leak request A's key to request B.
The correct shape is the v8.5.0 credentials_resolver seam on the
providers. Pass a zero-arg callable that returns the credentials for the
current request; it is invoked fresh on every model call and the
resolved secret is never stored on the provider instance:
from symfonic.core.providers import AnthropicProvider
# resolver reads the request-scoped credential (contextvars / in-flight request)
provider = AnthropicProvider(
credentials_resolver=lambda: {"api_key": current_tenant_api_key()},
)
Resolution precedence is credentials_resolver() > static token >
env lookup. The static token kwarg is a single-tenant convenience and
a documented footgun under a shared provider — use the resolver for
anything multi-tenant. See
The Production-Adopter Pattern for
the end-to-end wiring shapes.
The chat command¶
For a quick one-shot query against the library (no scaffold required):
symfonic chat "Hello" --tenant-id t1 # uses the mock provider
symfonic chat "What is DI?" --tenant-id t1 --model anthropic --enable-hms
symfonic chat "..." --tenant-id t1 --domain default
--model accepts mock (default) or anthropic; --enable-hms turns
on the HMS-aware system prompt; --domain selects a domain template.
This is for smoke-testing the engine, not for production traffic.
Next steps / where to customize¶
The generated project ships a CUSTOMIZE.md mapping each of the five
HMS memory layers to the file that shapes it. The four checkpoints to
start with:
- Identity —
app/domains/starter/plugin.py(personality, system prompt). - Tools —
app/domains/starter/tools.py(async functions asStructuredTools). - Rules —
app/domains/starter/rules.md(business rules; Deep Sleep promotes them to procedural memory). - Visual style —
webapp/static/css/custom.css(CSS variables;webapponly).
When you are ready to harden for production, walk the checklist in the
generated README.md and read
Level 5: Production.