Architecture (diagrams): docs/architecture.md — system overview, frontend modules, contact/chat flows, deploy topology.
Run the static site plus mock APIs on a single origin (http://localhost:8080).
docker compose up --buildOpen http://localhost:8080. The nginx proxy serves the repo root read-only and forwards POST /api/contact to the mock ingress and POST /api/chat to the chat service.
Stop and remove containers:
docker compose downSmoke checks (requires stack up):
./scripts/docker-smoke.shOverride the base URL: DOCKER_SMOKE_URL=http://127.0.0.1:8080 ./scripts/docker-smoke.sh.
Chat tests (no Docker required): from repo root, create a venv under docker/chat and run pytest (uses CHAT_PROVIDER=mock):
cd docker/chat && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt -r requirements-dev.txt
PYTHONPATH=. .venv/bin/python -m pytest tests -vEnvironment: The local stack does not require secrets. Optional compose overrides can live in a gitignored .env at the repo root; see docker/.env.example for a placeholder. Production and SAM variable names live in secrets.example/deploy.env.example.
This repo is a static site hosted on Amplify, with an AWS-native durable contact pipeline used to receive and deliver contact messages.
- Canonical template:
secrets.example/deploy.env.examplelists every name used for SAM deploy (export …in the file body). Copy to.secrets/deploy.env(gitignored), fill real values,chmod 600. Seesecrets.example/README.md. - Deploy:
scripts/integrate-and-deploy.sh—bash scripts/integrate-and-deploy.shor… stage. Omitted argument defaults to prod (SAM_STACK_NAME, defaultpage). Loads.secrets/deploy.envwhenRESEND_API_KEYis unset, then optional.secrets/chat-deploy.env. When.secrets/manifest.jsonandconfig.manifest.jsonboth exist, the script first runs Secrets Manager seed + push (same as the old orchestrate step); setSKIP_SECRETS_MANAGER=1to skip that (quick redeploys; CI has no manifests so it never runs there).chat.marwanelgendy.linkis the staging frontend;gvp:chat-api-urlmust target the chat API (execute-api URL from optional Lambda stack, ALB, etc.). Lambda + Gemini (recommended for staging): setCHAT_SAM_STACK_NAME_STAGE(stage) /CHAT_SAM_STACK_NAME_PROD(prod), or legacyCHAT_SAM_STACK_NAME, plusGEMINI_API_KEYin.secrets/chat-deploy.env— deploy runsaws/chat-template.yamlafter the contact stack and uses outputChatPostApiUrlfor meta whenCHAT_STAGE_CHAT_API_URLis unset. Pass a real AI Studio key at deploy (SAM parameterGeminiApiKeyhas no default). SAM-managed ECS (voice is always on in the FE): setCHAT_ECS_SAM_STACK_NAME_STAGE/CHAT_ECS_SAM_STACK_NAME_PROD,GEMINI_API_KEY, and VPC + subnet IDs — or rely on the defaultCHAT_VOICE_ECS_BOOTSTRAP=1to auto-fill default VPC, subnets, ECR repo, URI, and stack names (gvp-chat-ecs-{stage,prod}) (seechat-deploy.env.example). Opt out of all ECS auto-wiring (text-only chat on Lambda) withCHAT_VOICE_ECS_BOOTSTRAP=0. When HTML sync is on,CHAT_STAGE_CHAT_API_URLoverrides Lambda output; prod usesCHAT_PROD_CHAT_API_URLthenChatPostApiUrlfrom the same-run chat deploy when set. Actions → Integrate and deploy includes deploy_environment (prod/stage). HTML meta sync: locally,SYNC_API_URLSdefaults to on unless set to0; the GitHub Action sync_api_urls input defaults to off — enable it when you want the workflow to patchindex.html/admin/index.htmland upload the artifact. - GitHub Actions: create repository secrets with the same names as in
deploy.env.example(noexportprefix). Optional variables:AWS_REGION,SAM_STACK_NAME,SAM_STACK_NAME_STAGE,CHAT_SAM_STACK_NAME(fallback when per-env names unset),CHAT_SAM_STACK_NAME_STAGE,CHAT_SAM_STACK_NAME_PROD,CHAT_CORS_ORIGINS,GEMINI_MODEL,GEMINI_FALLBACK_MODEL(defaults in code/template togemini-3.1-flash-lite+gemma-4-26b-a4b-it), plusCHAT_ECS_SAM_STACK_NAME_STAGE,CHAT_ECS_SAM_STACK_NAME_PROD,CHAT_ECS_VPC_ID,CHAT_ECS_SUBNET_IDS,CHAT_ECR_REPOSITORY_URI,CHAT_ECR_REPO_NAME,CHAT_ECS_CREATE_DEFAULT_VPC,CHAT_VOICE_ECS_BOOTSTRAP, legacyCHAT_ECS_CLUSTER_*/CHAT_ECS_SERVICE_*, and other names fromchat-deploy.env.example. Optional secretGEMINI_API_KEYfor Lambda and ECS chat stacks. - Never commit real keys,
ADMIN_API_KEY, or production-only values..gitignoreexcludes.secrets/,.env*, credential JSON patterns, andaws/.env. Keepaws/samconfig.tomlfree of secrets; pass parameters via env /--parameter-overridesas the scripts do. - Public site analytics: events go to Google Analytics from the browser (
js/analytics.js+ gtag inindex.html). The private admin page has tabs for Contact (queue health, retries) and Transcripts (chat sessions with stream/voice rollups, activity sparkline, recent failures). - Contact API URL is in HTML
<meta name="gvp:contact-api-url">. After deploy, runscripts/sync-site-api-urls.mjs(or the workflow with sync_api_urls) to patch that meta from stack outputContactApiUrl. - Optional chat URL sync uses the same script:
node scripts/sync-site-api-urls.mjs <contactApiUrl> <chatApiUrl>. The second argument patches<meta name="gvp:chat-api-url">where present (index.html, andadmin/index.htmlif that meta exists).chatApiUrlis the chat API (FastAPIPOST /api/chat), not the staging site atchat.marwanelgendy.linkunless that host reverse-proxies/api/chatto the container. - Browser voice (Gemini Live): mic UI is always rendered in the FE — no meta flag. Voice transport needs a WebSocket-capable chat host with
CHAT_LIVE_RELAY=1(default indocker/chat/Dockerfilefor ECS images). API Gateway HTTP API + Lambda usesCHAT_LIVE_RELAY=0inaws/chat-template.yaml; the browser client does not open adirect_googleLive socket by default (avoids Google 1011); use ECS/ALB for real voice.integrate-and-deploy.shwarns when the resolvedgvp:chat-api-urlpoints at a Lambda execute-api host (text chat works, voice will fail there). OptionalCHAT_LIVE_VOICE_STRICT=1on the API makesPOST /api/live/sessionreturn 503 when relay is off (no token mint).integrate-and-deploy.shpatchesgvp:chat-api-urlfromCHAT_PROD_CHAT_API_URL/CHAT_STAGE_CHAT_API_URLor ECS ALB discovery (opt outCHAT_ECS_AUTO_SYNC_CHAT_URL=0). Repo variables for Actions:CHAT_VOICE_ECS_BOOTSTRAP,CHAT_ECS_AUTO_SYNC_CHAT_URL,CHAT_LIVE_VOICE_STRICT(optional).
- The UI shows success only after the backend has persisted the message and queued it for delivery.
- Delivery happens asynchronously through SQS + Lambda, so accepted messages are durable even if Resend is temporarily unavailable.
- Failures are retained, retried, and surfaced through DLQ alarms and report emails.
Use Actions → Integrate and deploy → Run workflow (sam build / sam deploy, optional Lambda chat stack, optional ECS chat image, optional HTML sync). Choose deploy_environment prod or stage. For stage, either set CHAT_STAGE_CHAT_API_URL to a full …/api/chat URL, or set CHAT_SAM_STACK_NAME_STAGE (or CHAT_SAM_STACK_NAME) + secret GEMINI_API_KEY so the workflow deploys aws/chat-template.yaml and can sync meta from ChatPostApiUrl.
- Repository variables (optional):
AWS_REGION,SAM_STACK_NAME,SAM_STACK_NAME_STAGE,CHAT_SAM_STACK_NAME,CHAT_SAM_STACK_NAME_STAGE,CHAT_SAM_STACK_NAME_PROD,CHAT_CORS_ORIGINS,CHAT_STAGE_CHAT_API_URL,CHAT_PROD_CHAT_API_URL,CHAT_ECS_AUTO_SYNC_CHAT_URL,CHAT_LIVE_VOICE_STRICT,CHAT_ECS_SAM_STACK_NAME_STAGE,CHAT_ECS_SAM_STACK_NAME_PROD,CHAT_ECS_VPC_ID,CHAT_ECS_SUBNET_IDS,CHAT_ECR_REPOSITORY_URI,CHAT_ECR_REPO_NAME,CHAT_ECS_CREATE_DEFAULT_VPC,CHAT_VOICE_ECS_BOOTSTRAP,CHAT_ECS_CLUSTER_*,CHAT_ECS_SERVICE_*, and other ECR/ECS names fromchat-deploy.env.examplewhen used. - Repository secrets (required for deploy): same names as
secrets.example/deploy.env.example—RESEND_API_KEY,CONTACT_TO_EMAIL,CONTACT_FROM_EMAIL,ALARM_EMAIL,ADMIN_API_KEY; optionalCONTACT_REPORT_EMAIL. OptionalGEMINI_API_KEYwhen deploying the Lambda chat stack. - AWS auth (workflow input): OIDC —
AWS_DEPLOY_ROLE_ARN; or keys —AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY. - Workflow input sync_api_urls: patches
index.htmlandadmin/index.htmland uploads artifactsite-html-api-urls.
cp -R secrets.example .secrets
cp secrets.example/deploy.env.example .secrets/deploy.env
# Edit .secrets/deploy.env; optional: manifest.json + config.manifest.json + .secrets/files/
chmod 600 .secrets/deploy.env .secrets/files/* 2>/dev/null || true
bash scripts/integrate-and-deploy.shOptional ECS networking (pin IDs in git or CI): npm run chat:discover-env prints export CHAT_ECS_VPC_ID=…, CHAT_ECS_SUBNET_IDS=…, CHAT_ECR_REPOSITORY_URI=… (same EC2 logic the deploy script uses when those vars are unset during SAM ECS). Append to .secrets/chat-deploy.env, review, chmod 600.
With manifest.json + config.manifest.json present under .secrets/, the same command uploads manifest file secrets and seeds config before SAM deploy. Override directory: SECRETS_DIR=/path/to/.secrets bash scripts/integrate-and-deploy.sh stage. Skip Secrets Manager prep: SKIP_SECRETS_MANAGER=1 bash scripts/integrate-and-deploy.sh.
IAM (caller): secretsmanager:CreateSecret, DescribeSecret, PutSecretValue for manifest secret IDs, plus sam deploy / CloudFormation permissions.
- Infrastructure:
aws/template.yaml(SAM). Lambda npm deps are declared inaws/src/package.json;sam buildinstalls them into each function artifact (npm run sam:buildfrom repo root). - Ingress Lambda:
aws/src/contact-ingress.js - Sender Lambda:
aws/src/contact-sender.js - Failure report Lambda:
aws/src/contact-report.js - Admin Lambda:
aws/src/contact-admin.js - DynamoDB: messages table has GSI
byCreatedAt(listPk+createdAt). New writes setlistPktoCONTACT. Existing rows withoutlistPkwill not appear in the admin message list until you run the one-offaws/src/backfill-listpk.js(see script header for env vars) aftercd aws/src && npm install. - Frontend route:
/api/contact(default whengvp:contact-api-urlmeta is empty), or set meta /window.__CONTACT_API_URL__viascripts/sync-site-api-urls.mjs
Resend requires that the from address is verified (domain or sender). Example:
CONTACT_FROM_EMAIL=Marwan <[email protected]>
- Messages are stored in DynamoDB before delivery is attempted.
- Delivery work is queued in SQS and processed by a sender Lambda.
- Failed deliveries retry automatically through SQS redrive policy.
- Messages that continue to fail end up in a DLQ, trigger CloudWatch alarms, and are included in a scheduled failure report.
- Static page:
admin/index.html - Client:
js/admin.js - Styles:
css/admin.css - Contact routes:
GET /api/contact/admin/summary,messages(supports?limit,?cursor+ JSONnextCursorfor Load older messages),messages/{id},health;POST /api/contact/admin/retry/{id},messages/{id}/suppress-report - Chat transcript routes (same Lambda):
GET /api/chat/admin/transcripts(paginated),transcripts/{id},transcripts/summary(top-levelstream+voicerollups,activityByDay,recentFailures[]);POST /api/chat/admin/transcripts/{id}/note,transcripts/{id}/reviewed - Chat host diagnostic (separate origin, chat ECS task):
GET /api/chat/host-statusreturns provider/model info and liveTranscriptStorecounters. RequiresADMIN_API_KEYenv on the chat container; the admin SPA falls back to DDB-only rollups when unset. - Auth: header
x-admin-keymatchingADMIN_API_KEYfrom deploy env
Scheduled failure reports (aws/src/contact-report.js) only include non-sent messages with attempts > 0 and exclude items where reportSuppressed is true.
Every text chat turn (success, error, or timeout) writes a row before the user-visible response returns. Per-turn fields the admin panel renders:
stream,firstTokenLatencyMs,streamChunkCount,outputCharCount,fallbackUsedstatus(ok/error/timeout),errorCode,errorMessage- existing:
latencyMs,toolCalls,actions,retrieval,flags
transcripts/summary rolls these up into streamedSessions, streamedTurns, streamFailures, streamTimeouts, fallbackTurns, avgFirstTokenLatencyMs, avgOutputChars, errorsByCode, and the 20 most-recent recentFailures[] (clickable in the panel to jump to the transcript).