Skip to content

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-compose component)

Install

pip install symfonic-core

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:

symfonic --help
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.

migrate is a placeholder — it prints "not yet implemented" and exits. chat is a one-shot query runner covered at the end of this guide. The command you want here is init.

symfonic init — Command Reference

symfonic init [PROJECT_NAME] [OPTIONS]
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

symfonic init -i

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

symfonic init --list-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:

  • onboarding requires auth. Onboarding keys tenants off the logged-in user, so if you omit auth the onboarding files are dropped even if you list them.
  • store-starter replaces the generic starter domain. When it is active, app/domains/starter/ is omitted and app/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 and webapp are 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:

symfonic init demo-app --components fastapi,auth --llm-provider anthropic --output-dir .

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.:

symfonic-core[agent-api,postgres,prompts,anthropic,ask-user]>=9.0.0,<10.0

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 run make migrate before the first boot and on every deploy. The generated README.md carries 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:

  1. Identityapp/domains/starter/plugin.py (personality, system prompt).
  2. Toolsapp/domains/starter/tools.py (async functions as StructuredTools).
  3. Rulesapp/domains/starter/rules.md (business rules; Deep Sleep promotes them to procedural memory).
  4. Visual stylewebapp/static/css/custom.css (CSS variables; webapp only).

When you are ready to harden for production, walk the checklist in the generated README.md and read Level 5: Production.