Project B, Generative Dashboard (AI Assistant): System Design & Administrator Manual
Scope. This document covers Project B only, the Generative Dashboard capability, surfaced in the product as the Chat assistant. That means the
/chatsubpage, the AI assistant it drives (running on the sharedai-service), the chart and dashboard artifacts it produces (/dashboard,/dashboard/{id}), the public share flow (/share/{token}), the back-end proxy endpoints that reach the assistant, the live Peec data it grounds its charts in, and the database tables that store conversations and dashboards.Project B is one of two AI features that are cleanly separated by their own subpages. The other, Prompt Suggestion (the
/prompt-suggestionstab, backed by the separateprompt_suggestion_service), is Project A and is documented inprompt-suggestion/system-design-and-admin-manual.md. Static browse/analytics pages that ship with mock data (/mydashboard,/overview,/prompts) are not part of Project B and are intentionally out of scope.It is written to be understood by both non-technical readers (start with the "In plain language" boxes and the diagrams) and technical readers (tables, env vars, and file references throughout).
Part 1: System Design
1.1 Overview
In plain language. The Generative Dashboard is the feature behind the Chat tab. You ask the assistant a question about how your brand shows up in AI answers (e.g. "how did my visibility trend last month vs my competitors?"), and instead of only writing a paragraph, it builds charts on the spot from your real Peec data and shows them inside the conversation. You can then pin any chart to a dashboard you keep and revisit, rearrange the tiles, and share a read-only view by link, with an optional chat next to it.
Technically, Project B is an agentic, tool-calling pipeline exposed as
server-sent-event (SSE) streaming endpoints on the shared ai-service:
- Converse: a LangGraph "deep agent" (built with
deepagents) receives the user turn plus a system prompt, the conversation history, and a set of tools. It streams tokens back as it thinks and acts. - Retrieve: the agent pulls live brand data through the Peec MCP server
(
mcp-server-peecai), not from a local copy, so numbers are always current. - Render:
render_*tools do not take data; they take a data recipe (ordered MCP calls loaded into DuckDB tables plus a SQLSELECT). The recipe is resolved against live data into a typedComponentResponsechart spec. Charts forbid hard-coded dates (:start_date/:end_dateplaceholders) so a pinned chart re-resolves fresh data every time it is viewed. - Persist: the transcript is written to the
Messagetable and the agent's working memory to the LangGraph checkpointer tables; charts the user keeps becomePinnedChartrows on aDashboard.
The assistant is internal-only: the browser never talks to ai-service
directly. It sits behind the Node/Express back-end (apps/server), which
authenticates the user, validates the request, and proxies the SSE stream.
1.2 Components & responsibilities
| Component | Tech | Port | Role in Project B |
|---|---|---|---|
Web app (apps/web) |
React 19 + Vite, served by nginx | 80 | The Chat subpage (/chat), the dashboard grid (/dashboard, /dashboard/{id}), and the public share view (/share/{token}). Uses assistant-ui with an ExternalStoreRuntime; streams over SSE; renders render_* charts with the same primitives as the rest of the app. |
Back-end / BFF (apps/server) |
Node 20, Express 5, TypeScript | 4000 | The only public entry point. Authenticates (Firebase), validates (Zod), rate-limits, and proxies the SSE stream to the assistant, re-validating every frame. Routers: /api/threads, /api/dashboard, /api/dashboards, /api/dashboard-templates, /api/charts, /api/share. |
AI service (apps/ai-service, Project B brain) |
Python 3.12, FastAPI, LangGraph, deepagents |
8000 | Runs the deep agent: tool selection, chart synthesis, dashboard mutations, conversation persistence, title generation. Streaming endpoints: /api/threads/message/stream, /api/threads/{id}/message/stream, /api/share/chat/stream; plus /api/threads/{id}/message, /api/charts/resolve, /api/completions, /api/embeddings. |
| PostgreSQL + pgvector | pgvector/pgvector:pg16 |
5432 | Stores Thread / Message (transcript), Dashboard / PinnedChart / DashboardShare (artifacts), and the LangGraph checkpointer tables (checkpoints, checkpoint_blobs, checkpoint_writes) that hold the agent's per-thread memory. |
| Peec MCP server (external) | mcp-server-peecai via https://api.peec.ai/mcp |
443 | The live source of brand/visibility data every chart is grounded in. Reached only by ai-service. |
| LLM providers (external) | OpenAI / Google | 443 | The models that power the agent. Provider is inferred from the model id (gpt-* → OpenAI, gemini* → Google). |
In plain language: the Chat tab is the storefront, the Express server is the
security desk that checks your badge and relays the live feed, ai-service is
the analyst who actually reads your data and draws the charts, the Peec MCP
server is the filing cabinet of real numbers, and Postgres is the notebook where
your conversations and saved dashboards are kept.
1.3 Hardware / software mapping
Everything runs as Docker containers orchestrated by Docker Compose, on a single Coolify-managed Linux host (VPS). Containers share the host's CPU/RAM and are isolated at the container level. The only outbound "hardware" dependencies are third-party SaaS (OpenAI/Google, the Peec MCP/API, Langfuse, Firebase).
| Software unit | Runs as | Host resource | External hardware/SaaS reached |
|---|---|---|---|
ai-service (Project B brain) |
Docker container (python:3.12-slim, uvicorn) |
CPU during chart resolution (DuckDB); mostly I/O-wait on the LLM and MCP | OpenAI / Google LLMs; Peec MCP (api.peec.ai/mcp); Langfuse (traces) |
server |
Docker container | low; holds the SSE proxy open per active stream | Firebase (token verification) |
postgres (pgvector) |
Docker container | Disk (named volume postgres-data) + RAM |
none |
web |
Docker container (nginx static) | low | none |
| Traefik (provided by Coolify) | Host reverse proxy | network / TLS | Let's Encrypt (certificates) |
Project B is CPU-light and I/O-heavy: most wall-clock time is spent waiting on the LLM and on Peec MCP calls. Turns are long-lived SSE streams rather than single request/response round-trips, so the
serverholds a proxy connection open for the duration of each turn.
1.4 Deployment diagram
flowchart TB
internet(["Internet (HTTPS)"])
subgraph public["Published via Traefik (public subdomains)"]
direction TB
traefik["Traefik reverse proxy (Coolify)<br/>TLS via Let's Encrypt"]
web["web · nginx :80<br/>React SPA · Chat + Dashboards + Share"]
server["server · Express :4000<br/>Firebase auth · Zod · rate-limit · SSE proxy"]
end
subgraph internal["Internal Docker network (never exposed to the internet)"]
direction TB
ai["ai-service · FastAPI :8000 (PROJECT B)<br/>LangGraph deep agent · render_* tools<br/>/api/threads/message/stream · /api/threads/{id}/message/stream<br/>/api/share/chat/stream · /api/charts/resolve · /health"]
pg[("postgres / pgvector :5432<br/>Thread · Message · Dashboard · PinnedChart<br/>DashboardShare · LangGraph checkpointer")]
end
subgraph external["External SaaS / APIs"]
direction TB
mcp["Peec MCP server (api.peec.ai/mcp)<br/>live brand / visibility data"]
llm["OpenAI / Google"]
langfuse["Langfuse (LLM tracing)"]
firebase["Firebase (auth)"]
end
internet --> traefik
traefik -->|"pr-N.preview… (dev.*)"| web
traefik -->|"api-pr-N.preview… (dev-api.*)"| server
web -.->|"browser → VITE_API_URL (SSE)"| server
server -->|"internal only · SSE proxy"| ai
server -->|"token verify"| firebase
ai -->|"chat completions"| llm
ai -->|"chart data (recipes)"| mcp
ai -->|"transcript + checkpointer + dashboards"| pg
ai --> langfuse
Only web and server are published through Traefik (they get public
subdomains). ai-service and postgres are never exposed to the internet. They
are reachable only on the internal Docker network (expose: not ports:).
⚠️ Local caveat:
docker-compose.yml(development only) publishesai-serviceon host port 8000 for convenience. The production (docker-compose.coolify.yml) and preview compose files useexpose:only. See §1.7 and §2.4.
1.5 Persistent data
In plain language: unlike Prompt Suggestion, Project B does keep state: your conversations and your saved dashboards. Most of it lives in the shared Postgres database; the assistant's data itself (the brand numbers) is always fetched live and never copied.
| Data | Where it lives | Owned by | Notes |
|---|---|---|---|
Conversation transcript (Thread, Message) |
Postgres (postgres-data volume) |
schema owned by apps/server (Prisma); written by both apps/server (Prisma) and ai-service (asyncpg) |
Thread links to a Firebase userId and optionally a dashboardId; Message stores role/content/tool-call metadata. Soft-deleted (deletedAt), restorable. |
Agent working memory (checkpoints, checkpoint_blobs, checkpoint_writes) |
Postgres | schema owned by Prisma; written by LangGraph (AsyncPostgresSaver) |
Keyed by thread_id == Thread.id. This is how a follow-up turn "remembers" the conversation. ai-service verifies this schema's version at readiness (/health/ready). |
Dashboards & charts (Dashboard, PinnedChart) |
Postgres | schema owned by apps/server; written by both |
PinnedChart holds a spec (the data recipe, re-resolved on view), a snapshot (last resolved result, used as fallback), plus layout (grid geometry) and presentation (per-chart visual edits). Identity is (dashboardId, toolCallId). |
Share links (DashboardShare) |
Postgres | apps/server |
token + accessLevel (public or members, default members) + a frozen viewState (time range). |
| Rich chat UI state | Browser localStorage (peec.chat.sessions.v1) |
the browser only | Tool-call pills, multi-chart ordering, and "interrupted" turns are reconstructed from this local blob, not the server. A cache clear or device switch loses that fidelity (see §2.4). |
| Brand / visibility data | External Peec MCP server | Peec platform | Fetched live per chart resolution; never stored by Project B. |
| LLM traces | Langfuse (external SaaS) | ai-service | Observability only. |
ai-serviceowns no Prisma schema and runs no migrations (repo convention: onlyapps/serverowns the schema). It connects to the same database to read and writeThread/Message/Dashboard/PinnedChartand the checkpointer tables, but never runs DDL.Statefulness: Project B is stateful (conversations and dashboards persist). Containers themselves are stateless and can be restarted or scaled horizontally, but the Postgres volume is the source of truth and must be backed up.
1.6 Access control
In plain language: you must be logged in to chat and to see your dashboards. Login is checked at the front door (the Express server) and again by the assistant for chat actions. Shared dashboards can be opened by link, with an optional "members only" restriction.
| Layer | Mechanism | Notes |
|---|---|---|
| Browser → server | Firebase Authentication (ID token as Authorization: Bearer …) |
Verified by requireAuth (apps/server/src/middleware/auth.middleware.ts). Missing token → 401, invalid/expired → 403. Plus a global per-user rate limit (100) and per-route limiters (create-conversation, send-prompt, shared-chat). |
| Share links | optionalAuth |
GET /api/share/:token and POST /api/share/:token/chat verify a token if present but never require one. accessLevel: "members" returns 401 without a logged-in user; "public" is open to anyone with the link. |
| Server → ai-service | Internal Docker network; Authorization + x-request-id forwarded |
Chat/thread/persona routes on ai-service do verify Firebase (Depends(get_current_user)). POST /api/charts/resolve, GET /api/brands/metadata, and POST /api/share/chat/stream are intentionally unauthenticated (internal-only; share-chat authZ is enforced by the server). |
| ai-service → Peec MCP | PEECAI_API_KEY + PEECAI_PROJECT_ID (service credentials) |
Server-to-server; scopes MCP data to the project. |
| ai-service → LLM providers | Provider API keys (OPENAI_API_KEY, …) |
Server-to-server. |
| Ownership | userOwnsThread, userOwnsDashboard, fetch_owned_dashboard |
Threads and dashboards are ownership-checked; foreign/unknown ids return 404 with no data leak. A follow-up turn ignores a client-supplied dashboardId and uses the stored Thread.dashboardId. |
1.7 Security
Network isolation (strength). ai-service and Postgres are not
internet-reachable in production. Only web and server are published via
Traefik. All inter-service traffic stays on the private Docker network. TLS is
terminated at Traefik with automatically renewed Let's Encrypt certificates.
Authentication & ownership (strength). Unlike Prompt Suggestion, Project B
verifies Firebase auth end-to-end for chat, and enforces per-user ownership
on threads and dashboards (userOwnsThread / userOwnsDashboard /
fetch_owned_dashboard), returning 404 for anything the caller does not own,
with no data leak.
Grounded, date-safe charts (strength). Every chart is resolved from live
Peec MCP data through a SQL recipe; assert_no_literal_dates forbids hard-coded
dates so pinned charts always re-resolve current data rather than freezing stale
numbers. SSE frames are validated against SseEventSchema in the proxy and
malformed frames are dropped.
Input validation (strength). The server validates every request with Zod and
tool payloads are validated against ComponentResponseSchema before rendering.
Unauthenticated calls get 401; bad input gets 400.
Secrets. LLM keys (OPENAI_API_KEY, GOOGLE_API_KEY),
the Peec key (PEECAI_API_KEY), and the Firebase service account
(FIREBASE_SERVICE_ACCOUNT_BASE64) are injected as environment variables
(GitHub Actions secrets → Coolify env), never committed.
⚠️ Known security gaps (see also §2.4). Honest limitations of the current design, called out so administrators can compensate:
ai-serviceis published to the host in local Compose.docker-compose.ymlmapsports: 8000:8000"for dev convenience." Because a fewai-serviceendpoints are unauthenticated by design (/api/charts/resolve,/api/brands/metadata,/api/share/chat/stream), exposing that port in a real environment would make them directly reachable. Mitigation: production/preview compose already useexpose:only; keep port 8000 firewalled and never copy the dev port mapping to prod.- Some
ai-serviceendpoints have no auth of their own. They rely on the service being internal-only and on the BFF enforcing share authorization. Mitigation: keep the service internal-only; treat the BFF as the sole trust boundary; consider a service token for defense-in-depth.- No SSE heartbeat or idle-timeout. The proxy sets
Connection: keep-alivebut emits no server ping and has no configurable stream timeout; a stalled LLM or MCP call can hang a turn, and the client does not reconnect. Mitigation: add a heartbeat/idle-timeout tosseProxy.tsand a bounded upstream timeout; surface a retry in the UI.- Rich chat history is browser-only. The
Messagetable stores flat text + one component per message; tool-call pills and multi-chart ordering live only inlocalStorage. A device switch or cache clear loses fidelity. Mitigation: persist structured turn parts server-side if durability is required.- Auto-pin failures are swallowed.
auto_pin_chartlogs and returns on any error ("auto-pin must never fail the render"), so a chart can appear in chat but silently fail to pin to the dashboard. Mitigation: surface a non-blocking warning when a pin fails.- Rate limits are per-instance, in-memory. They reset on restart and are not shared across replicas. Mitigation: move to a shared store if the service is scaled horizontally.
Part 2: Administrator Manual
2.1 Infrastructure setup
In plain language: one Linux server runs Coolify (a self-hosted deployment platform, similar to a private Heroku). Coolify runs Traefik for routing/TLS and builds/starts the containers from the repo's Compose files.
Prerequisites on the host
- A Linux VPS with Docker Engine + Docker Compose v2.
- Coolify installed (provides the Traefik reverse proxy and the
coolifyDocker network that the Compose files attach to). - DNS: a wildcard
*.preview.peecai-csee.com(and thedev.*/dev-api.*records) pointing at the host, so Traefik can route per-environment subdomains. - Firewall: expose only 80/443 publicly. Ports 8000 (
ai-service) and 5432 (Postgres) must not be publicly reachable.
Build toolchain (for local/CI builds)
- Node.js 20+, pnpm 9.12.0 (via Corepack),
uv(Python), Docker Compose v2.
Compose files (what runs where)
| File | Used for | Key differences |
|---|---|---|
| docker-compose.yml | Local development | Publishes host ports (including ai-service 8000); Grafana anonymous-admin; loads each service's local .env. |
| docker-compose.coolify.yml | Production (Coolify) | Services expose only (no host ports); secrets required. |
| docker-compose.preview.yml | Per-PR preview envs | Traefik labels for pr-N.* / api-pr-N.*; attaches to the external coolify network. |
2.2 Deployment steps
A. Production (Coolify)
- In Coolify, the application points at this repo and uses
docker-compose.coolify.yml. - Set all required environment variables in the Coolify UI (see §2.3): the LLM
provider key(s),
PEECAI_API_KEY+PEECAI_PROJECT_ID, the Firebase service account,DATABASE_URL, andCORS_ALLOWED_ORIGINS. - Coolify builds the images and starts the stack.
serverdepends on a healthyai-service, which depends on a healthypostgres. - Migrations gate this feature. On boot the
servercontainer runsprisma migrate deploy, which creates/updatesThread,Message,Dashboard,PinnedChart,DashboardShare, and the checkpointer tables.ai-servicerefuses to be ready (/health/ready) until the checkpointer schema matches its expected version, so the agent cannot run against an un-migrated database. - Traefik publishes
webandserverwith Let's Encrypt TLS.ai-servicecomes up on the internal network athttp://ai-service:8000. - Verify:
GET /health(and/health/ready) onai-servicefrom inside the network, then the Chat tab end-to-end from the browser (send a message, confirm a chart renders and can be pinned).
B. Preview deployments (per pull request)
Driven by .github/workflows/preview-deploy.yml. Every PR labelled
deploy-preview gets its own throwaway copy of the whole stack at its own URL,
and it is deleted when the PR closes.
- Trigger: add the
deploy-previewlabel to a PR (or push to an already-labelled PR). - The workflow SSHes to the host,
rsyncs the repo, and writes a.envfrom thePREVIEW_ENVsecret plusVITE_API_URL=https://api-pr-<N>.preview.peecai-csee.comandVITE_APP_ENV=dev. - It brings the stack up with a dedicated project name
(
docker compose -f docker-compose.preview.yml -p preview-pr-<N>). - Clean slate each deploy: the previous stack is torn down with its DB
volume (
down --volumes), so every preview starts with empty chat history and no dashboards. - Migration self-heal: if Prisma is wedged (P3009), the workflow rolls back the named failed migrations and retries once.
- A bot comments the preview URL and sets a
preview: live/preview: failedlabel from a health check. - Teardown: closing the PR (or removing the label) runs
cleanup-preview, whichdown --volumes --rmi alland deletes the preview directory.
Preview data caveat: because the DB volume is wiped on every preview deploy, conversations and dashboards do not carry over between deploys. Charts still resolve live from the Peec MCP server, so generation works from a clean database.
2.3 Environment variables
AI service (ai-service) (app/settings.py; UPPER_CASE as env vars):
| Variable | Default | Purpose |
|---|---|---|
AI_DEFAULT_MODEL |
gpt-5.4-mini |
Default agent model. Provider is inferred from the id (gpt-* → OpenAI, gemini* → Google). |
OPENAI_API_KEY / GOOGLE_API_KEY |
(unset) | Provider credentials. At least the one matching AI_DEFAULT_MODEL (and any model the UI offers) must be set. |
EMBEDDING_MODEL |
text-embedding-3-small |
Embedding model (OpenAI). |
LLM_MAX_RETRIES |
6 |
Retries on a failing LLM call (rate-limit/transient). |
AGENT_RECURSION_LIMIT |
80 |
LangGraph step budget per turn (guards runaway tool loops). |
PEECAI_API_KEY |
(unset) | Credential for the Peec MCP server (the chart data source). Required for real charts. |
PEECAI_PROJECT_ID |
(unset) | Scopes MCP data to a project; required for project-scoped keys. |
PEEC_API_URL |
https://api.peec.ai |
Base URL for the Peec MCP/API. |
DATABASE_URL |
(compose-provided) | Postgres connection (transcript, dashboards, checkpointer). |
FIREBASE_SERVICE_ACCOUNT_BASE64 / FIREBASE_WEB_API_KEY |
(unset) | Verifies user tokens on authenticated chat routes. |
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL |
(unset) | LLM tracing (optional). |
PEEC_KNOWLEDGE_DIR / _SITEMAP_URL / _PATH_PREFIXES / _CACHE_TTL_SECONDS |
(unset) | Read-only knowledge corpus mounted for the agent. |
ENVIRONMENT / LOG_LEVEL |
local / info |
Environment label and log verbosity. |
Back-end (server) (src/lib/env.ts; validated at boot):
| Variable | Purpose |
|---|---|
AI_SERVICE_URL |
Base URL of ai-service (Compose sets http://ai-service:8000). The generative-dashboard upstream. |
DATABASE_URL |
Postgres connection (Prisma). |
FIREBASE_SERVICE_ACCOUNT_BASE64 |
Verifies user tokens (base64 service-account JSON). |
CORS_ALLOWED_ORIGINS |
Allowed browser origins (required in prod). |
SHUTDOWN_TIMEOUT_MS / SLOW_REQUEST_MS / LOG_LEVEL / PORT |
Graceful shutdown, slow-request logging, verbosity, port. |
Web (build-time, baked into the bundle):
| Variable | Purpose |
|---|---|
VITE_API_URL |
BFF base URL the SPA (and the SSE client) call. |
VITE_APP_ENV |
Environment label (local / dev / prod). |
2.4 Known issues & workarounds
| # | Issue | Impact | Workaround / fix |
|---|---|---|---|
| 1 | ai-service port 8000 published in local Compose. |
If copied to a real env, unauthenticated endpoints (/api/charts/resolve, /api/brands/metadata, /api/share/chat/stream) become directly reachable. |
Production/preview compose use expose: only; keep 8000 firewalled. Never carry the dev port mapping into prod. |
| 2 | No SSE heartbeat / idle-timeout; no client reconnect. | A stalled LLM or MCP call can hang a turn indefinitely; a dropped socket ends the turn with no auto-retry. | Add a heartbeat + bounded idle-timeout in sseProxy.ts; add a client retry affordance. |
| 3 | Rich chat history lives only in the browser (peec.chat.sessions.v1). |
Cache clear / device switch loses tool-call pills, multi-chart ordering, and "interrupted" turns; only flat text + one component per message is server-side. | Persist structured turn parts server-side if durability is needed. |
| 4 | Auto-pin failures are swallowed. | A chart renders in chat but silently fails to pin to the dashboard. | Surface a non-blocking warning; log-and-alert on repeated failures. |
| 5 | Rate limits are per-instance, in-memory. | Limits reset on restart and are not shared across replicas. | Move to a shared/distributed store before scaling out. |
| 6 | PEECAI_API_KEY / PEECAI_PROJECT_ID unset disables live data. |
Charts cannot resolve real numbers; the assistant loses its grounding. | Set both in every real environment. |
| 7 | Migrations gate readiness. | If the checkpointer schema is not migrated, ai-service /health/ready fails and the agent will not run. |
Ensure prisma migrate deploy ran on server boot before expecting the assistant to work. |
| 8 | Static mock pages coexist with the real feature (/mydashboard, /overview, /prompts). |
Operators/users may mistake mock pages for live data. | Treat only /chat + /dashboard + /share as the live generative-dashboard surface. |
| 9 | Preview deploy wipes the DB volume each run. | Conversations and dashboards do not persist across preview deploys. | Expected for throwaway previews; charts still resolve live. |
| 10 | Long turns hold a server proxy connection open. | Many concurrent long turns tie up server connections. |
Monitor concurrent stream count; combine with the timeout fix (#2). |
2.5 Third-party components
| Component | Type | Used for | Where configured |
|---|---|---|---|
| OpenAI | External API | LLM completions + embeddings | OPENAI_API_KEY, AI_DEFAULT_MODEL, EMBEDDING_MODEL |
| Google (Gemini) | External API | Optional LLM provider (gemini* models) |
GOOGLE_API_KEY |
Peec MCP server (mcp-server-peecai, api.peec.ai/mcp) |
External API (MCP) | The live brand/visibility data every chart is grounded in | PEECAI_API_KEY, PEECAI_PROJECT_ID, PEEC_API_URL |
LangGraph / deepagents |
Python libraries | The agent runtime, tool orchestration, and Postgres checkpointer | apps/ai-service/app/llm |
| DuckDB | Embedded engine | Resolving chart data recipes (SQL over MCP results) | app/llm/resolver.py |
assistant-ui |
Frontend library | The chat runtime/UI on the web app | apps/web/src/features/assistant-chat |
| Langfuse | External SaaS | LLM trace/observability | LANGFUSE_* |
| Firebase Authentication | External SaaS (Google) | User identity / token verification | FIREBASE_SERVICE_ACCOUNT_BASE64 |
| PostgreSQL 16 + pgvector | Container image | Transcript, dashboards, and agent checkpointer | pgvector/pgvector:pg16, DATABASE_URL |
| FastAPI / Pydantic v2 / uv | Python libraries | Service framework + dependency manager | apps/ai-service |
| Express 5 / Prisma | Node libraries | BFF + database access / migrations | apps/server |
| Coolify | Self-hosted PaaS | Build & deploy orchestration on the host | host |
| Traefik | Reverse proxy | Routing + TLS termination for public services | provided by Coolify; labels in docker-compose.preview.yml |
| Let's Encrypt | CA | Automatic TLS certificates | Traefik certresolver=letsencrypt |
| Docker / Docker Compose | Runtime | Containerization & orchestration | all compose files |
| GitHub Actions | CI/CD | Preview deploys, migrations, checks | .github/workflows/ |
| Grafana + Loki + Promtail | Observability stack | Log aggregation/dashboards (platform-wide; Project B logs flow here) | infra/ + compose |
2.6 Quick operational reference
- Health:
GET http://ai-service:8000/healthand/health/ready(internal; readiness also checks the checkpointer schema version). - Primary endpoints (reached by the browser through the server as
POST /api/threadsandPOST /api/threads/{id}/message):POST /api/threads/message/stream(new thread) andPOST /api/threads/{id}/message/stream(follow-up) onai-service. - Chart refresh:
POST /api/charts/resolvere-resolves a pinned chart's recipe against current data; the server falls back to the storedsnapshot(returns502) if the upstream fails. - OpenAPI/Swagger: FastAPI serves
/openapi.json,/docs,/redoconai-service(internal); the server serves Swagger UI at/api-docsin non-production. - Logs:
docker compose -p <project> logs -f ai-service(andserver); aggregated in Grafana/Loki. - Restart safely: containers are stateless, but conversations and
dashboards persist in Postgres, so back up the
postgres-datavolume. After a schema change, ensure migrations are applied before serving. - Full reference: the agent architecture guide is
apps/ai-service/app/llm/AGENTS.md.