Skip to content

Project A — Prompt Suggestion: System Design & Administrator Manual

Scope. This document covers Project A only — the Prompt Suggestion capability. That means the prompt_suggestion_service microservice and everything directly wired to it: the browser tab that drives it, the back-end proxy endpoints that reach it, the LLM/embedding service it calls, the vector database it reads, and the external Peec data API it enriches from. Other parts of the wider platform (dashboards, chat, crawl analytics, etc.) 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. "Prompt Suggestion" is the feature that proposes new search-style questions ("prompts") a brand should track — e.g. "best free project management software". A user opens the Prompt Suggestions tab, optionally tweaks some inputs (their markets, competitors, target audience), and clicks Generate. Behind the scenes the system looks at how real people phrase questions in this category, asks a Large Language Model (LLM) to write fresh, on-topic prompts, removes duplicates, and returns a diverse list the user can accept and start tracking.

Technically, Project A is a retrieval-augmented generation (RAG) pipeline exposed as a stateless Python microservice:

  1. Retrieve — embed a category query and run a vector similarity search over a pool of real-user "clickstream" reference prompts stored in pgvector.
  2. Enrich — pull the brand's profile, existing tracked prompts, and topics from the Peec customer API for framing.
  3. Synthesize — ask the LLM (via the shared ai-service) to write new prompts, one batch per confirmed topic, steered by the references + framing.
  4. Diversify — exact-text dedup, then per-topic MMR selection with a cross-topic near-duplicate guard, to return a varied final set.

The service is internal-only: the browser never talks to it directly. It sits behind the Node/Express back-end (apps/server), which authenticates the user and proxies the request.

1.2 Components & responsibilities

Component Tech Port Role in Project A
Web app (apps/web) React 19 + Vite, served by nginx 80 The Prompt Suggestions tab (/prompt-suggestions): the generation wizard, the "Configure Inputs" panel, the coverage map, and duplicate detection UI.
Back-end / BFF (apps/server) Node 20, Express 5, TypeScript 4000 The only public entry point. Authenticates the user (Firebase), validates the request (Zod), and proxies verbatim to Project A. Endpoints: POST /api/suggestions, POST /api/topics, POST /api/prompt-similarity.
Prompt Suggestion service (apps/prompt_suggestion_service) — Project A Python 3.12, FastAPI, Pydantic v2, httpx 8001 The RAG orchestration: retrieval + enrichment + synthesis + diversity. Owns all prompt/template logic.
AI service (apps/ai-service) Python 3.12, FastAPI 8000 Shared LLM gateway. Project A calls it for embeddings (POST /api/embeddings) and completions (POST /api/completions). It brokers the actual OpenAI calls.
PostgreSQL + pgvector pgvector/pgvector:pg16 5432 The vector store of reference/clickstream prompt embeddings that retrieval reads.
Peec customer API (external) https://api.peec.ai/customer/v1 443 Source of brand profile, existing prompts, and topics for framing/enrichment.

In plain language: the browser tab is the storefront, the Express server is the security desk that checks your badge and passes your order through, Project A is the kitchen that actually prepares the suggestions, the AI service is the appliance the kitchen uses, and the vector database + Peec API are the pantry of ingredients.

1.3 Hardware / software mapping

Everything runs as Docker containers orchestrated by Docker Compose, on a single Coolify-managed Linux host (VPS). There is no per-service dedicated hardware; containers share the host's CPU/RAM and are isolated at the container level. The only outbound "hardware" dependencies are third-party SaaS (OpenAI, Peec API, Langfuse).

Software unit Runs as Host resource External hardware/SaaS reached
prompt-suggestion-service (Project A) Docker container (python:3.12-slim, uvicorn) CPU-bound during synthesis; mostly I/O-wait on the LLM none directly — reaches OpenAI through ai-service
ai-service Docker container CPU/network OpenAI / Google LLM & embedding APIs; Langfuse (traces)
postgres (pgvector) Docker container Disk (named volume postgres-data) + RAM
server Docker container low Firebase (token verification)
web Docker container (nginx static) low
Traefik (provided by Coolify) Host reverse proxy network / TLS Let's Encrypt (certificates)

Project A is CPU-light and I/O-heavy: most wall-clock time is spent waiting on the LLM. A single /suggestions call was measured at ~40 s end-to-end (meta.duration_ms ≈ 39,825, reference_count = 45).

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 · Prompt Suggestions tab"]
        server["server · Express :4000<br/>Firebase auth · Zod validation · verbatim proxy"]
    end

    subgraph internal["Internal Docker network (never exposed to the internet)"]
        direction TB
        pss["prompt-suggestion-service · FastAPI :8001 (PROJECT A)<br/>POST /suggestions /topics /similarity<br/>/retrieve /synthesize /test/* · GET /health"]
        ai["ai-service :8000<br/>/api/embeddings · /api/completions"]
        pg[("postgres / pgvector :5432<br/>reference prompt embeddings")]
    end

    subgraph external["External SaaS / APIs"]
        direction TB
        peec["Peec customer API (api.peec.ai/customer/v1)<br/>profile · prompts · topics"]
        llm["OpenAI / Google"]
        langfuse["Langfuse (LLM tracing)"]
    end

    internet --> traefik
    traefik -->|"pr-N.preview… (dev.*)"| web
    traefik -->|"api-pr-N.preview… (dev-api.*)"| server
    web -.->|"browser → VITE_API_URL"| server
    server -->|"internal only"| pss
    pss -->|"embeddings + completions"| ai
    pss -->|"vector search"| pg
    pss -->|"brand context"| peec
    ai --> llm
    ai --> langfuse

Only web and server are published through Traefik (they get public subdomains). Project A, ai-service, and postgres are never exposed to the internet — they are reachable only on the internal Docker network (expose: not ports:).

1.5 Persistent data

In plain language: Project A itself keeps almost nothing — it's a "calculate and return" service. The data it uses lives elsewhere.

Data Where it lives Owned by Notes
Reference / clickstream prompt embeddings pgvector table(s) in PostgreSQL (postgres-data volume) populated by an ingestion process (not by Project A at request time) Read-only for Project A during retrieval. This is the "memory" of how real users phrase questions.
Brand profile, existing prompts, topics External Peec customer API Peec platform Fetched live per request; not stored by Project A. Best-effort — failures degrade gracefully (empty context) except the profile, which is required.
Accepted suggestions → tracked prompts TrackedPrompt / Topic tables (owned by apps/server via Prisma) apps/server When the user accepts prompts, the server persists them — Project A is not involved in the write.
LLM traces Langfuse (external SaaS) ai-service Observability only.

Project A owns no Prisma schema and runs no migrations (repo convention: only apps/server owns the schema). It connects to the same database purely to read the vector store.

Statefulness: Project A is effectively stateless between requests — it holds only in-process HTTP clients and config. It can be restarted or scaled horizontally without data-loss concerns.

1.6 Access control

In plain language: you must be logged in to use the feature. Login is checked once, at the front door (the Express server). Project A trusts that the server already did the checking.

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.
Server → Project A Internal Docker network only The token is forwarded, but Project A does not verify it (see Security).
Project A → Peec API PEEC_API_KEY (service credential) Server-to-server; not user-scoped.
Project A → ai-service Internal network, no auth Both internal-only.

1.7 Security

Network isolation (strength). Project A, ai-service, and Postgres are not internet-reachable — 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.

Input validation (strength). The server validates every request with Zod before proxying (SuggestionRequestSchema, TopicGenerationRequestSchema). Bad input is rejected fast with a clean 400 ValidationError; unauthenticated calls get 401. (Verified live: missing project_id, limit out of the 1–50 range, branded_ratio > 1, and malformed JSON all return 400; no token → 401.)

Secrets. LLM keys (OPENAI_API_KEY), the Peec key (PEEC_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). These are honest limitations of the current design, called out so administrators can compensate:

  1. No authorization on project_id (tenant isolation gap). The server authenticates who you are but does not check that the project_id you ask for belongs to you before proxying to Project A. Any logged-in user can request suggestions/topics/personas for any project id. Verified: calling the personas path with a project id the account does not own returned 200 with data. Mitigation: add an ownership check in the server controllers (suggestions/topics/prompt-similarity) before the proxy call; return 404 for foreign projects (as the dashboards code already does).
  2. Project A has no authentication of its own — auth was explicitly deferred (see the service's CLAUDE.md: "Auth: removed for now"). The only gate is the server. Mitigation: keep the service internal-only (already the case) and reintroduce a token check for defense-in-depth.
  3. No end-to-end timeout on the server→Project A call. The server's fetch helper has no AbortSignal; a stalled LLM can hang the request (synthesis can already legitimately take ~40 s). Mitigation: add a bounded timeout that maps to the existing 502 path.

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 coolify Docker network that the Compose files attach to).
  • DNS: a wildcard *.preview.peecai-csee.com (and the dev.* / dev-api.* records) pointing at the host, so Traefik can route per-environment subdomains.
  • Firewall: expose only 80/443 publicly. Ports 8001 (Project A), 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; Grafana anonymous-admin; Project A reads apps/prompt_suggestion_service/.env. | | docker-compose.coolify.yml | Production (Coolify) | Services expose only (no host ports); secrets required; LLM_PROVIDER=remote. | | 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)

  1. In Coolify, the application points at this repo and uses docker-compose.coolify.yml.
  2. Set all required environment variables in the Coolify UI (see §2.3).
  3. Coolify builds the images (Project A via its Dockerfile: python:3.12-slim + uv sync --frozen) and starts the stack.
  4. Traefik publishes web and server on their configured domains with Let's Encrypt TLS. Project A comes up on the internal network at http://prompt-suggestion-service:8001.
  5. The server container applies Prisma migrations on boot (prisma migrate deploy) before serving. Project A needs no migration step.
  6. Verify: GET /health on Project A (from inside the network) and the Prompt Suggestions tab end-to-end from the browser.

B. Preview deployments (per pull request)

Driven by .github/workflows/preview-deploy.yml. In plain language: every PR labelled deploy-preview gets its own throwaway copy of the whole stack at its own URL, and it's deleted when the PR closes.

  1. Trigger: add the deploy-preview label to a PR (or push to an already-labelled PR).
  2. The workflow SSHes to the host, rsyncs the repo to /var/www/previews/pr-<N>/, and writes a .env from the PREVIEW_ENV secret plus:
  3. VITE_API_URL=https://api-pr-<N>.preview.peecai-csee.com
  4. VITE_APP_ENV=dev
  5. It brings the stack up with a dedicated project name (docker compose -f docker-compose.preview.yml -p preview-pr-<N>), building Project A and the others fresh.
  6. Clean slate each deploy: the previous stack is torn down with its DB volume (down --volumes) so every preview starts from a clean schema.
  7. Migration self-heal: if Prisma is wedged in a failed state (P3009), the workflow rolls back the named failed migrations and retries once.
  8. A bot comments the preview URL (https://pr-<N>.preview.peecai-csee.com) and sets a preview: live / preview: failed label based on a health check.
  9. Teardown: closing the PR (or removing the label) runs cleanup-preview, which down --volumes --rmi all and deletes the preview directory.

Routing for a preview is done by Traefik labels in docker-compose.preview.yml: webpr-<N>.preview.peecai-csee.com (port 80), serverapi-pr-<N>.preview.peecai-csee.com (port 4000). Project A stays internal.

Preview data caveat: because the DB volume is wiped on every preview deploy, the pgvector reference prompt data must be (re)seeded for retrieval to return meaningful results in a preview. With an empty vector store, generation still runs but has no clickstream references to ground on.

2.3 Environment variables

Project A — prompt_suggestion_service (app/settings.py; snake_case on the wire, UPPER_CASE as env vars):

Variable Default Purpose
LLM_PROVIDER mock mock returns canned data (tests); must be remote in real envs to call the LLM. Coolify/preview set remote.
LLM_MODEL gpt-4o-mini Model label reported in meta.llm_model.
AI_SERVICE_URL (unset) Base URL of the ai-service for embeddings (/api/embeddings) + completions (/api/completions). Compose sets http://ai-service:8000; required when LLM_PROVIDER=remote.
VECTOR_STORE pgvector Vector backend (only pgvector supported).
DATABASE_URL postgresql://peec:peec@localhost:5432/peec Postgres/pgvector connection.
PEEC_API_BASE_URL https://api.peec.ai/customer/v1 Brand profile/prompts/topics source.
PEEC_API_KEY (unset) Credential for the Peec API. Unset → profile enrichment unavailable (the required-profile path returns 502; prompts/topics degrade to empty).
REQUEST_TIMEOUT_MS 30000 httpx timeout for outbound calls.
SYNTHESIS_MAX_CONCURRENCY 5 Cap on parallel per-topic LLM batches.
MMR_LAMBDA 0.5 Relevance-vs-diversity tradeoff in selection.
MMR_CROSS_TOPIC_GUARD 0.90 Cosine threshold above which a cross-topic near-duplicate is dropped.
TOPIC_DEDUP_THRESHOLD 0.85 Similarity threshold for topic dedup.
LLM_MAX_ATTEMPTS 3 Retries for a failing LLM call.
SYNTHESIS_TEMPERATURE (unset) Optional LLM temperature override (0–2).
SYNTHESIS_MASK_ENABLED / _DROP_COUNT / _SEED false / 1 / 0 Optional context-masking to decorrelate brand vocabulary across topics (PEEC-418).

Connected components (subset relevant to Project A):

Variable Service Purpose
OPENAI_API_KEY / GOOGLE_API_KEY ai-service LLM & embedding provider credentials (Project A reaches these through ai-service).
AI_DEFAULT_MODEL (gpt-5.4-mini) / EMBEDDING_MODEL (text-embedding-3-small) ai-service Default completion / embedding models.
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL ai-service LLM tracing (optional).
PROMPT_SUGGESTION_SERVICE_URL server, ai-service Where to reach Project A (http://prompt-suggestion-service:8001).
FIREBASE_SERVICE_ACCOUNT_BASE64 server Verifies user tokens (base64-encoded service-account JSON).
CORS_ALLOWED_ORIGINS server Allowed browser origins (e.g. the preview/dev web domain).
VITE_API_URL / VITE_APP_ENV web (build-time) API base URL baked into the SPA; environment label.

2.4 Known issues & workarounds

# Issue Impact Workaround / fix
1 project_id not authorized at the server before proxying. Cross-tenant access: any logged-in user can generate/read for any project id (verified). Add an ownership check in the suggestions/topics/prompt-similarity controllers; return 404 for foreign projects. Keep Project A internal-only.
2 Project A has no auth (deferred by design). Sole gate is the server; no defense-in-depth. Never expose 8001 publicly (already the case). Reintroduce a token check when the scheme is decided.
3 No timeout on server→Project A fetch. A stalled LLM hangs the request; the UI shows "Generating…" indefinitely. Add an AbortController timeout in the server's fetch helper → maps to 502, which the wizard's retry UI already handles.
4 Slow generation (~40 s) with faint progress UI. Users may think it's stuck. Strengthen the in-progress indicator (persistent + elapsed time); consider a cancel button.
5 Inline "Generate more prompts" fails silently (onError only clears the spinner). On failure the user gets no message (unlike the wizard, which shows a retry). Surface an error/toast on the inline path too.
6 LLM_PROVIDER defaults to mock. If unset in a real env, Project A returns canned data with no error. Ensure LLM_PROVIDER=remote in every non-test environment (Coolify/preview already set it).
7 PEEC_API_KEY unset disables profile enrichment. The required-profile path returns 502; prompts/topics context is dropped. Set PEEC_API_KEY in all real environments.
8 Per-topic synthesis cost scales with the number of confirmed topics (each batch re-sends the full reference pool; up to 2+2 backfill rounds). Latency and LLM spend grow with topic count. Cap confirmed_topics per request server-side; tune SYNTHESIS_MAX_CONCURRENCY.
9 Preview deploy wipes the DB volume each run. pgvector reference data is empty until reseeded → ungrounded retrieval. Reseed the vector store as part of preview provisioning if grounded results are needed.
10 Prisma P3009 wedged migrations could previously block deploys. Deploy failures on the server (Project A unaffected). Already mitigated: the preview workflow auto-rolls-back failed migrations and retries once.

2.5 Third-party components

Component Type Used for Where configured
OpenAI External API LLM completions + embeddings (via ai-service) OPENAI_API_KEY, AI_DEFAULT_MODEL, EMBEDDING_MODEL
Google (Gemini) External API Optional LLM provider (via ai-service) GOOGLE_API_KEY
Peec customer API (api.peec.ai/customer/v1) External API Brand profile, existing prompts, topics PEEC_API_BASE_URL, PEEC_API_KEY
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 Vector store for reference prompts pgvector/pgvector:pg16, DATABASE_URL
FastAPI / Pydantic v2 / httpx / uv Python libraries The service framework + HTTP client + dependency manager apps/prompt_suggestion_service
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 A logs flow here) infra/ + compose

2.6 Quick operational reference

  • Health: GET http://prompt-suggestion-service:8001/health (internal).
  • Primary endpoint: POST /suggestions (reached by the browser as POST /api/suggestions through the server).
  • OpenAPI/Swagger: FastAPI serves /openapi.json, /docs, /redoc on Project A (internal).
  • Logs: docker compose -p <project> logs -f prompt-suggestion-service; aggregated in Grafana/Loki.
  • Restart safely: Project A is stateless — restart/redeploy freely; no data migration or drain needed.
  • Full reference: docs/prompt-suggestion/documentation.md (service spec, architecture, data model).