| title | Render |
|---|
import { Aside, TabItem, Tabs } from "@astrojs/starlight/components";
The primary deployment path for this project. Free web services, no credit card, deploys straight from a GitHub repo.
The repo already contains deploy/render.yaml, so the settings are in
version control rather than in someone's memory.
- Render → New + → Blueprint
- Pick your repo
- Render reads the file and asks you for the three secret values
- Apply
Render → New + → Web Service → connect your repo, then:
| Field | Value |
|---|---|
| Root Directory | app |
| Runtime | Python |
| Build Command | pip install uv && uv sync --frozen |
| Start Command | uv run fastapi run main.py --port $PORT |
| Instance Type | Free |
| Health Check Path | /health |
services:
- type: web
name: research-agent
runtime: python
plan: free
rootDir: app
buildCommand: pip install uv && uv sync --frozen
startCommand: uv run fastapi run main.py --port $PORT
healthCheckPath: /health
envVars:
- key: LLM_API_KEY
sync: false
- key: SUPABASE_URL
sync: false
- key: SUPABASE_SECRET_KEY
sync: false
- key: TAVILY_API_KEY
sync: false
- key: LLM_MODEL
value: gemini-flash-latest
- key: LLM_PROVIDER
value: google
- key: MAX_AGENT_STEPS
value: "12"
- key: MAX_THREAD_TURNS
value: "10"
- key: ALLOWED_ORIGINS
value: "*"Four things in there are worth knowing:
rootDir: apppoints Render at the uv project —pyproject.toml,uv.lock, and the app code all live inapp/, not the repo root. Every other path in the blueprint, includingbuildCommandandstartCommand, resolves relative to it.uv sync --frozenmeans "useuv.lockexactly, don't re-resolve". Your deploy gets the versions you tested, not whatever is newest today.sync: falsemeans "ask me for this value in the dashboard, and never store it in the repo". This is how secrets stay out of git — the four keys above it are secret, so none of them carry a value here.TAVILY_API_KEYis the one that's also optional: leave it empty in the dashboard and the agent runs on Wikipedia alone.- The variables that do carry a value (
LLM_MODEL,LLM_PROVIDER,MAX_AGENT_STEPS,MAX_THREAD_TURNS,ALLOWED_ORIGINS) aren't secret, so committing them is fine. ChangeALLOWED_ORIGINSto your real frontend origin before you go public.
Dashboard → your service → Environment.
| Variable | Required | Notes |
|---|---|---|
LLM_API_KEY |
yes | from AI Studio, or whichever provider you chose |
SUPABASE_URL |
yes | no trailing slash |
SUPABASE_SECRET_KEY |
yes | the sb_secret_… key, not the publishable one. On a pre-rename project, the legacy service_role key works here too |
LLM_MODEL |
on Google only | defaults to gemini-flash-latest; required for every other provider |
LLM_PROVIDER |
no | google (default), cerebras, openrouter, openai-compatible |
LLM_BASE_URL |
only for openai-compatible |
e.g. https://api.groq.com/openai/v1 |
TAVILY_API_KEY |
no | enables web search, so the agent can answer questions about recent events |
MAX_AGENT_STEPS |
no | defaults to 12 |
MAX_THREAD_TURNS |
no | defaults to 10 — the cap on one conversation |
ALLOWED_ORIGINS |
no | comma-separated, no trailing slash |
If a free key dies mid-demo, changing LLM_PROVIDER, LLM_API_KEY and
LLM_MODEL here and redeploying moves you to another provider without a code
change. /health reports the provider back so you can confirm it took.
The old GEMINI_API_KEY and GEMINI_MODEL names still work when
LLM_PROVIDER is google, so a service deployed before this change keeps
running untouched.
Setting a variable doesn't restart the service. If you get
Missing environment variable after adding one, trigger a manual deploy.
| Instance hours | 750 / month / workspace |
| Credit card | not required |
| Spin-down | after ~15 minutes idle |
| Cold start | ~1 minute |
| CPU | a small shared slice |
| Filesystem | ephemeral — wiped on every restart |
Two of these bite people:
The cold start. Hit /health ten minutes before any demo. A judge or
interviewer waiting 60 seconds on a blank page will assume it's broken.
The ephemeral filesystem. Anything written to disk is gone on restart. This is why the project keeps everything in Postgres and nothing on disk — it's not an accident.
The small CPU slice is fine here because agent work is I/O-bound; you're waiting on the model anyway. It would be bad for anything CPU-heavy.
Render shows you the build in real time. Worth knowing what you're looking at:
==> Cloning from https://github.com/...
==> Running build command 'pip install uv && uv sync --frozen'...
Resolved 94 packages in 12ms
Installed 94 packages in 1.2s
==> Build successful 🎉
==> Deploying...
==> Running 'uv run fastapi run main.py --port $PORT'
INFO: Uvicorn running on http://0.0.0.0:10000
==> Your service is live 🎉
If it stops after "Deploying..." and eventually says something about ports, your
start command isn't binding 0.0.0.0.
The free tier sleeps. If you need it responsive:
- Hit
/healthbefore demos, manually. - Or schedule a GitHub Action to ping it. Note this consumes your 750 monthly instance hours faster — pinging every 10 minutes keeps it awake permanently, which is roughly 720 hours a month. That's within budget for one service, and over it if you run two.
Don't ping more often than you need to.
- Troubleshooting — when it doesn't work
- FastAPI Cloud — the alternative path