What's Happening
Every endpoint in main.py is completely open. The service is deployed publicly at https://dnb-ai.onrender.com (referenced in the CORS list, line 28, and the keep-awake workflow), and anyone who finds the URL can:
- Burn the project's Gemini quota and budget with unlimited
POST /chat calls — each one costs real money and there is no throttle of any kind
- Read other users' conversations:
chat_id is client-supplied (line 114) and the response returns the full history (lines 148-168), so anyone who knows or guesses a session ID gets the entire conversation
- Delete anyone's session via
DELETE /chat/{chat_id}
CORS (lines 21-33) only restrains browsers — curl and scripts ignore it entirely. This service is meant to be called by the DeenBridge backend/frontend, not the open internet, but nothing enforces that.
Where to Find This
main.py — CORS middleware (lines 21-33), /chat (lines 108-174), /chat/{chat_id} delete (lines 176-187), /ping (lines 102-105)
render.yaml — envVars block where the new secret must be declared (sync: false, like GEMINI_API_KEY)
.github/workflows/keep-render-awake.yml — the pinger must keep working (leave its target unauthenticated or pass the header)
What We Want
- Shared-secret authentication: require an
X-API-Key header on /chat and /chat/{chat_id}, validated against a SERVICE_API_KEY environment variable, returning 401 when missing/wrong. Implement with FastAPI's APIKeyHeader security scheme and a dependency, so the requirement shows up in /docs
- Leave the health/ping route unauthenticated so Render health checks and the keep-awake workflow keep working
- Per-client rate limiting on
/chat (e.g. slowapi): a sensible default like 20 requests/minute per API key (falling back to client IP), returning 429 with a Retry-After header
- If
SERVICE_API_KEY is unset, fail loudly at startup in production but allow an explicit opt-out for local development (e.g. AUTH_DISABLED=true), so contributors without secrets can still run the service
- Document the new header and env vars in the README, and add
SERVICE_API_KEY to render.yaml
Technical Context
slowapi is the standard FastAPI wrapper around limits; its default in-memory storage is fine for the current single-instance deployment — note in code comments that a shared Redis backend is needed if the session-persistence issue lands multi-instance support
- Render sits behind a proxy: use
X-Forwarded-For (Render sets it) rather than the socket peer address for IP-based limiting
- Use
secrets.compare_digest for the key comparison
- Comparing keys per-request is cheap; do not add token/JWT infrastructure — a single shared secret between the DeenBridge backend and this service is the right size for this architecture
Acceptance Criteria
POST /chat without X-API-Key (or with a wrong key) returns 401; with the correct key it works exactly as before
- The 21st request within a minute from the same key returns 429 with
Retry-After
- Health/ping remains reachable without a key; the keep-awake workflow still succeeds
- Local development without any secret is possible via the documented opt-out
- README and
render.yaml document SERVICE_API_KEY
What's Happening
Every endpoint in
main.pyis completely open. The service is deployed publicly athttps://dnb-ai.onrender.com(referenced in the CORS list, line 28, and the keep-awake workflow), and anyone who finds the URL can:POST /chatcalls — each one costs real money and there is no throttle of any kindchat_idis client-supplied (line 114) and the response returns the fullhistory(lines 148-168), so anyone who knows or guesses a session ID gets the entire conversationDELETE /chat/{chat_id}CORS (lines 21-33) only restrains browsers — curl and scripts ignore it entirely. This service is meant to be called by the DeenBridge backend/frontend, not the open internet, but nothing enforces that.
Where to Find This
main.py— CORS middleware (lines 21-33),/chat(lines 108-174),/chat/{chat_id}delete (lines 176-187),/ping(lines 102-105)render.yaml—envVarsblock where the new secret must be declared (sync: false, likeGEMINI_API_KEY).github/workflows/keep-render-awake.yml— the pinger must keep working (leave its target unauthenticated or pass the header)What We Want
X-API-Keyheader on/chatand/chat/{chat_id}, validated against aSERVICE_API_KEYenvironment variable, returning 401 when missing/wrong. Implement with FastAPI'sAPIKeyHeadersecurity scheme and a dependency, so the requirement shows up in/docs/chat(e.g.slowapi): a sensible default like 20 requests/minute per API key (falling back to client IP), returning 429 with aRetry-AfterheaderSERVICE_API_KEYis unset, fail loudly at startup in production but allow an explicit opt-out for local development (e.g.AUTH_DISABLED=true), so contributors without secrets can still run the serviceSERVICE_API_KEYtorender.yamlTechnical Context
slowapiis the standard FastAPI wrapper aroundlimits; its default in-memory storage is fine for the current single-instance deployment — note in code comments that a shared Redis backend is needed if the session-persistence issue lands multi-instance supportX-Forwarded-For(Render sets it) rather than the socket peer address for IP-based limitingsecrets.compare_digestfor the key comparisonAcceptance Criteria
POST /chatwithoutX-API-Key(or with a wrong key) returns 401; with the correct key it works exactly as beforeRetry-Afterrender.yamldocumentSERVICE_API_KEY