> ## Documentation Index
> Fetch the complete documentation index at: https://docs.emergence.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Starter Templates

> Copy-paste scaffolds for three common solution shapes — minimal API service, multi-component (api + worker + DB), and agentic A2A solution.

# Starter Templates

Three tiers, increasing in complexity. Each tier is a starting point — copy, then mutate to fit. The Tier 1 fixtures are mirrored in `scripts/tests/fixtures/solution-dev/tier1/` and verified by `npm run check-snippets` so this page does not silently rot.

<Note>
  **Future**: when the `em-service-template` repo lands, these tiers collapse to `cookiecutter gh:emergenceai/em-service-template --no-input solution_name=<your-name> tier=<1|2|3>`. The inline scaffolds here are the bridge until that ships.
</Note>

<Tip>
  Every scaffold pins `em-service` to **v0.0.15** (the version in [em-service Chart](/deployment/helm/em-service) as of 2026-05-14). Major em-service version bumps trigger a fixture refresh; `npm run check-snippets` (CI) fails if the inline snippets drift from the canonical fixtures.
</Tip>

<Tabs>
  <Tab title="Tier 1 — Minimal API service">
    The simplest viable solution: one HTTP service, one image, JWT validation, env-injected secrets. Maps to the [Quickstart](/guides/solution-dev/quickstart) shape with auth and secrets wired in.

    ### File tree

    ```text theme={null}
    <solution>/
    ├── packages/api/
    │   ├── pyproject.toml
    │   └── src/api/
    │       ├── __init__.py
    │       ├── main.py
    │       └── auth.py
    ├── Dockerfile
    └── charts/<solution>/
        ├── Chart.yaml
        ├── values.yaml
        └── values.dev.yaml
    ```

    ### Files

    <CodeGroup>
      ```toml packages/api/pyproject.toml theme={null}
      [project]
      name = "<solution>-api"
      version = "0.1.0"
      requires-python = ">=3.12"
      dependencies = [
        "fastapi>=0.110",
        "uvicorn[standard]>=0.30",
        "python-jose[cryptography]>=3.3",
        "httpx>=0.27",
      ]

      [build-system]
      requires = ["hatchling"]
      build-backend = "hatchling.build"

      [tool.hatch.build.targets.wheel]
      packages = ["src/api"]
      ```

      ```python packages/api/src/api/main.py theme={null}
      from fastapi import Depends, FastAPI
      from typing import Annotated
      from .auth import current_user, project_id

      app = FastAPI(title="<solution>")

      @app.get("/healthz")
      async def healthz() -> dict[str, str]:
          return {"status": "ok"}

      @app.get("/echo")
      async def echo(
          msg: str,
          user: Annotated[dict, Depends(current_user)],
          proj: Annotated[str, Depends(project_id)],
      ) -> dict[str, str]:
          return {"echo": msg, "user_sub": user["sub"], "project_id": proj}
      ```

      ```python packages/api/src/api/auth.py theme={null}
      import os
      import time
      from typing import Annotated
      import httpx
      from fastapi import Header, HTTPException, status
      from jose import jwt, JWTError

      KEYCLOAK_ISSUER_URL = os.environ["KEYCLOAK_ISSUER_URL"]
      KEYCLOAK_AUDIENCE   = os.environ["KEYCLOAK_AUDIENCE"]

      # JWKS TTL — Keycloak rotates signing keys (default ~12h)
      _JWKS_TTL_SECONDS = 600
      _jwks_cache: dict | None = None
      _jwks_cached_at: float = 0.0

      def _jwks() -> dict:
          global _jwks_cache, _jwks_cached_at
          now = time.monotonic()
          if _jwks_cache is None or now - _jwks_cached_at > _JWKS_TTL_SECONDS:
              url = f"{KEYCLOAK_ISSUER_URL.rstrip('/')}/protocol/openid-connect/certs"
              _jwks_cache = httpx.get(url, timeout=5.0).raise_for_status().json()
              _jwks_cached_at = now
          return _jwks_cache

      def current_user(authorization: Annotated[str | None, Header()] = None) -> dict:
          if not authorization or not authorization.lower().startswith("bearer "):
              raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing bearer token")
          try:
              return jwt.decode(
                  authorization.split(" ", 1)[1],
                  _jwks(),
                  algorithms=["RS256"],
                  audience=KEYCLOAK_AUDIENCE,
                  issuer=KEYCLOAK_ISSUER_URL,
              )
          except JWTError as e:
              raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {e}")

      def project_id(x_project_id: Annotated[str | None, Header()] = None) -> str:
          if not x_project_id:
              raise HTTPException(status.HTTP_400_BAD_REQUEST, "Missing X-Project-ID header")
          return x_project_id
      ```

      ```dockerfile Dockerfile theme={null}
      FROM python:3.12-slim AS build
      WORKDIR /app
      RUN pip install --no-cache-dir uv
      COPY packages/api/pyproject.toml packages/api/pyproject.toml
      COPY packages/api/src packages/api/src
      RUN uv pip install --system --no-cache-dir ./packages/api

      FROM python:3.12-slim
      COPY --from=build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
      COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn
      EXPOSE 8000
      CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
      ```

      ```yaml charts/<solution>/Chart.yaml theme={null}
      apiVersion: v2
      name: <solution>
      description: A minimal CRAFT solution
      type: application
      version: 0.1.0
      appVersion: "0.1.0"
      dependencies:
        - name: em-service
          alias: api
          version: 0.0.15
          repository: oci://ghcr.io/emergenceai/em-charts
      ```

      ```yaml charts/<solution>/values.yaml theme={null}
      api:
        fullnameOverride: <solution>-api
        image:
          repository: <your-registry>/<solution>-api
          tag: 0.1.0
        service:
          type: ClusterIP
          port: 8000
        env:
          LOG_LEVEL: "INFO"
          KEYCLOAK_ISSUER_URL: "https://keycloak.example.com/realms/acme"
          KEYCLOAK_AUDIENCE: "<solution>-api"
        readinessProbe: { httpGet: { path: /healthz, port: 8000 } }
        livenessProbe:  { httpGet: { path: /healthz, port: 8000 } }
      ```

      ```yaml charts/<solution>/values.dev.yaml theme={null}
      api:
        image: { tag: dev }
        replicaCount: 1
        env:
          LOG_LEVEL: "DEBUG"
          KEYCLOAK_ISSUER_URL: "http://localhost:8080/realms/dev"
      ```
    </CodeGroup>

    ### Mirrored fixtures

    Canonical copies of the eight files above live at `scripts/tests/fixtures/solution-dev/tier1/`. `npm run check-snippets` compares the inline snippets to the fixtures and fails if they drift.
  </Tab>

  <Tab title="Tier 2 — API + worker + own DB">
    Adds a worker component, a Postgres database, and Alembic migrations. Mirrors the [Data Governance (em-data-readiness)](/data-governance/overview) shape.

    Below is the **diff** from Tier 1 — apply on top of Tier 1 to get Tier 2.

    ### File tree (additions)

    ```text theme={null}
    <solution>/
    ├── packages/
    │   ├── api/                          (unchanged from Tier 1)
    │   ├── worker/
    │   │   ├── pyproject.toml
    │   │   └── src/worker/
    │   │       ├── __init__.py
    │   │       └── main.py
    │   └── common/
    │       ├── pyproject.toml
    │       └── src/common/
    │           ├── __init__.py
    │           ├── db.py
    │           └── models.py
    ├── alembic.ini
    ├── alembic/
    │   ├── env.py
    │   └── versions/
    │       └── 0001_initial.py
    └── charts/<solution>/
        ├── Chart.yaml                    (add em-service alias for worker)
        └── values.yaml                   (add worker block + DATABASE_URL secret)
    ```

    ### Chart.yaml diff

    ```yaml charts/<solution>/Chart.yaml theme={null}
    apiVersion: v2
    name: <solution>
    type: application
    version: 0.1.0
    dependencies:
      - name: em-service
        alias: api
        version: 0.0.15
        repository: oci://ghcr.io/emergenceai/em-charts
      # NEW for Tier 2
      - name: em-service
        alias: worker
        version: 0.0.15
        repository: oci://ghcr.io/emergenceai/em-charts
    ```

    ### values.yaml diff

    ```yaml charts/<solution>/values.yaml theme={null}
    api:
      # ... existing Tier 1 api block ...
      envVars:
        # NEW: DB connection string from secret
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: <solution>-secrets
              key: database-url

    # NEW for Tier 2
    worker:
      image:
        repository: <your-registry>/<solution>-worker
        tag: 0.1.0
      service:
        enabled: false
      env:
        LOG_LEVEL: "INFO"
      envVars:
        - name: DATABASE_URL
          valueFrom: { secretKeyRef: { name: <solution>-secrets, key: database-url } }
    ```

    ### Worker entry point

    ```python packages/worker/src/worker/main.py theme={null}
    import asyncio
    import logging
    import os
    from common.db import session_factory

    logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))
    log = logging.getLogger("worker")

    async def process_one() -> None:
        async with session_factory() as session:
            # ... pull a job, run it, commit ...
            log.info("processed job")

    async def main() -> None:
        while True:
            try:
                await process_one()
            except Exception:
                log.exception("worker iteration failed")
            await asyncio.sleep(1.0)

    if __name__ == "__main__":
        asyncio.run(main())
    ```

    ### Common DB module

    ```python packages/common/src/common/db.py theme={null}
    import os
    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

    engine = create_async_engine(os.environ["DATABASE_URL"], echo=False, future=True)
    session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    ```

    Reference implementation: see [Data Governance overview](/data-governance/overview) — the `em-data-readiness` repo follows this exact pattern.
  </Tab>

  <Tab title="Tier 3 — Agentic A2A solution">
    Adds an A2A agent component with LiteLLM and (optionally) a Prefect flow. Mirrors the [Data Insights (em-talk2data)](/data-insights/overview) shape.

    Below is the **diff** from Tier 2.

    ### File tree (additions)

    ```text theme={null}
    <solution>/
    ├── packages/
    │   ├── api/         (unchanged)
    │   ├── worker/      (unchanged)
    │   ├── common/      (unchanged)
    │   ├── agent/
    │   │   ├── pyproject.toml
    │   │   └── src/agent/
    │   │       ├── __init__.py
    │   │       ├── card.py        # Agent Card author
    │   │       ├── llm.py         # LiteLLM client wrapper
    │   │       └── server.py      # A2A protocol server
    │   └── flows/                 # optional Prefect flows
    │       ├── pyproject.toml
    │       └── src/flows/
    │           ├── __init__.py
    │           └── pipeline.py
    └── charts/<solution>/
        ├── Chart.yaml             (add em-service alias for agent + optional flows)
        └── values.yaml            (add agent block with LLM env)
    ```

    ### Chart.yaml diff

    ```yaml charts/<solution>/Chart.yaml theme={null}
    dependencies:
      - { name: em-service, alias: api,    version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts }
      - { name: em-service, alias: worker, version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts }
      # NEW for Tier 3
      - { name: em-service, alias: agent,  version: 0.0.15, repository: oci://ghcr.io/emergenceai/em-charts }
    ```

    ### values.yaml additions for the agent

    ```yaml charts/<solution>/values.yaml theme={null}
    agent:
      image:
        repository: <your-registry>/<solution>-agent
        tag: 0.1.0
      service:
        type: ClusterIP
        port: 8001
      env:
        LOG_LEVEL: "INFO"
        AGENT_NAME: "<solution>-agent"
        LLM_GATEWAY_URL: "https://litellm.example.com/v1"
        LLM_DEFAULT_MODEL: "gemini-3.5-flash"
        LANGFUSE_HOST: "https://langfuse.example.com"
      envVars:
        - name: LLM_GATEWAY_API_KEY
          valueFrom: { secretKeyRef: { name: <solution>-secrets, key: llm-gateway-api-key } }
        - name: LANGFUSE_PUBLIC_KEY
          valueFrom: { secretKeyRef: { name: <solution>-secrets, key: langfuse-public-key } }
        - name: LANGFUSE_SECRET_KEY
          valueFrom: { secretKeyRef: { name: <solution>-secrets, key: langfuse-secret-key } }
      readinessProbe: { httpGet: { path: /.well-known/agent-card.json, port: 8001 } }
      livenessProbe:  { httpGet: { path: /.well-known/agent-card.json, port: 8001 } }
    ```

    ### Agent Card

    ```python packages/agent/src/agent/card.py theme={null}
    AGENT_CARD = {
        "name": "<solution>-agent",
        "description": "<one-line description>",
        "version": "0.1.0",
        "url": "https://<solution>-agent.example.com",
        "protocolVersion": "0.3",
        "capabilities": {"streaming": True, "pushNotifications": False},
        "skills": [
            {
                "id": "answer",
                "name": "Answer questions",
                "description": "Answers questions using the project's data",
                "tags": ["qa"],
                "examples": ["What's the trend in last quarter's sales?"],
            }
        ],
        "securitySchemes": {"bearer": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}},
        "security": [{"bearer": []}],
    }
    ```

    ### Register the agent

    Follow [Register Your First Agent](/guides/first-agent) — the agent must be registered with Assets after first deployment so other services and the UI can discover it.

    ### Reference implementation

    `em-talk2data` is the canonical reference — see [Data Insights overview](/data-insights/overview) for the full architecture (gateway service + insights agent + text2sql agent + LiteLLM via the same gateway pattern shown above).
  </Tab>
</Tabs>

## Snippet drift check

The Tier 1 fixture files at `scripts/tests/fixtures/solution-dev/tier1/` are the source of truth for the inline snippets above. Run `npm run check-snippets` to verify they match — CI runs this check on every PR.

If you change Tier 1 inline content, update the fixtures (or vice versa), then re-run `npm run check-snippets`.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/guides/solution-dev/quickstart">
    Walk through Tier 1 step by step.
  </Card>

  <Card title="Package and deploy" icon="box" href="/guides/solution-dev/package-and-deploy">
    Multi-component packaging and per-environment overlays.
  </Card>

  <Card title="Register your first agent" icon="robot" href="/guides/first-agent">
    Required for Tier 3 agents to be discoverable.
  </Card>

  <Card title="Data Insights overview" icon="chart-line" href="/data-insights/overview">
    The Tier 3 reference solution in production.
  </Card>
</CardGroup>
