Render · Opus 5.5 · medium effort · HIPAA organization · trial 1

API / Backend Service on Render

Success

Checks passed20/20
Human steps25 min of a person's time
Security & compliance71/100
Stack fit80/100
Build2m 34s
Deploy3m 58s
Tokens1.63M
Agent cost$1.26
Hosting / month$0.00 light use$7.13 busy

Deployed to https://adb-1slea-api-web.onrender.com. Every deployment is deleted after it's verified and reviewed.

Screenshots

The API documentation at /docs
The API documentation at /docs · https://adb-1slea-api-web.onrender.com/docs

Technologies

Runtime
Node.js 20+
Frameworks
Express
Data
PostgreSQL (Render Postgres, free)
Libraries
node-postgres
Packaging
Render Blueprint (render.yaml)
Hosting
Render web service (free, built from render-examples/express-hello-world)

Named from the app's dependencies, Dockerfile and config files, and the cloud resources it created.

Checks

Black-box checks against the live URL. A cell succeeds when every required check passes. The app's spec

ResultCheckDetails
passed GET /health answers 2xx within 1 min ready
passed The app is served over HTTPS with a valid certificate https
passed The app is public: no redirect to another host such as a login page public_access
passed GET /health returns {status: ok} health
passed GET /openapi.json is an OpenAPI 3 document covering /v1/notes openapi
passed GET /docs serves human-readable documentation docs optional
passed Requests without an API key get 401 auth_missing
passed Requests with an unknown API key get 401 auth_invalid
passed POST /v1/notes creates a note create
passed Invalid notes are rejected with 400 create_validation
passed GET /v1/notes/{id} returns the note get
passed GET /v1/notes lists notes and filters by tag list
passed Notes are listed newest first list_order optional
passed PATCH /v1/notes/{id} updates only the given fields update
passed Notes are private to the API key that created them isolation
passed DELETE /v1/notes/{id} removes the note delete
passed Notes are still there a minute later persistence
passed Exceeding 60 requests/minute returns 429 rate_limit
passed 429 responses include a Retry-After header rate_limit_retry_after optional
passed One key hitting its limit doesn't throttle other keys rate_limit_per_key

Human steps

Stack fit

The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.

Node/Express with Render Postgres is a simple, well-reasoned stack that fits this API. Its weak points are the expiring free database and a deployment that sidesteps Render's normal repo-driven builds.

CriterionWeightScoreWhy
Fit for the job 40 8/10 Express plus pg on managed Postgres suits a small authenticated JSON API with per-key data and a tag filter (TEXT[] with a GIN index). An in-memory rate limiter is fine for one instance. The free database expiring weakens the durability requirement.
Proportion 30 9/10 There are two runtime dependencies, a hand-written OpenAPI spec and HTML docs, and no extra services or frameworks, which matches the reasoning in STACK.md.
Platform fit 30 7/10 A Render web service with a health check and managed Postgres over the internal URL is a natural fit. But building from a borrowed example repo works against Render's git-based deploy model, and the render.yaml Blueprint the agent wrote was never actually used.

Security & compliance review

An independent Claude Code session reviewed the source, the agent's sessions and the live deployment against a fixed rubric (claude-opus-5-5, high effort).

The Notes API handles secrets carefully and the code is clean: parameterized SQL, owner-scoped access, input limits and no exposed files. Two deployment choices are the main problems. Production is built from Render's public example repo, with this app's code unpacked from a base64 env var, so the running code has no owner-controlled source of record. And the free Postgres expires on 2026-11-13, taking the 'persistent' notes with it. Smaller issues are missing security headers, a database open to outside connections by default, and no rate limit on unauthenticated requests.

CriterionWeightScoreWhy
Secrets handling 25 9/10 API keys and the DB connection string are kept only in Render service env vars and read from process.env. The transcripts never print them: the agent used a umask-077 temp file, deleted it afterwards and checked API_KEYS with cmp. No secrets are in the source. The one weak spot is a deploy script that can source a local .env and push secrets as plain env values instead of using Render's fromDatabase/secret-file wiring.
Deployment provenance 20 4/10 The live service builds from a third-party repo (render-examples/express-hello-world, unpinned main branch). Its build command deletes that code and unpacks this app from a base64 tarball stored in an env var. The real source exists only as unpushed local commits, so nothing the owner controls in version control shows what runs. Dependencies are lockfile-installed with npm ci, but NODE_VERSION floats on major 20.
Terms compliance 15 6/10 The use is personal, which Render's free tier allows. Sleeping after 15 minutes idle is tolerable for friends' scripts. But the free Postgres expires on 2026-11-13 and its data is then deleted, which conflicts with the requirement that notes persist. The agent did disclose this and give an upgrade step.
Least privilege 15 7/10 Render's native Node runtime runs the app without the agent managing a container, and the app connects over the internal URL. However, the Postgres was created without an ipAllowList, so on Render's default its external endpoint accepts connections from any IP that has the password. The app also connects as the database owner.
Transport security 10 8/10 HTTPS works with a valid Google Trust Services certificate, and HTTP gets a 301 redirect. There is no HSTS, X-Content-Type-Options, frame protection or CSP. The app could have added these (e.g. with helmet) but did not.
Attack surface 10 9/10 Probes for .env, .git/config, package.json, src files, /admin and /debug all return a JSON 404. Malformed JSON gets a clean 400 with no stack trace, and x-powered-by is disabled. Only the documented endpoints are exposed.
Application security 5 8/10 The app validates input with type and length limits, uses parameterized SQL, scopes every note by owner (other keys get 404), and escapes the docs HTML. Rate limiting works per key. Minor issues: requests with missing or invalid keys are never rate-limited, the raw API key is stored as the owner value, and the counters live in memory.

Findings

Cloud resources and cost

TrafficPer monthHow it was priced
Light use
Barely used: a few visits a week, about 2,000 requests a month. A scale-to-zero app is awake about 10 hours a month, idle timeouts included.
$0.00fixed monthly prices; 0.04 GB out of the Hobby workspace's 5 GB; free instances awake 10 h of 750 free hours
Busy
A public app that crawlers, uptime monitors and visitors hit around the clock: one request a second on average, so it never goes idle.
$7.13fixed monthly prices; 52.6 GB out of the Hobby workspace's 5 GB, 47.6 GB billed (with a payment method on file; without one Render suspends the services instead); free instances awake 730 h of 750 free hours

Resources the agent created

KindNamePlanList price / monthPricing
serviceadb-1slea-api-webfree$0.00free x 1 instance(s)
postgresadb-1slea-api-dbfree$0.00Postgres free

The agent estimated $0.00 a month.

Code

Code
689 lines in 15 files (JavaScript 600, Shell 89, Markdown 50, JSON 40, YAML 21)
Dependencies
2 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 34s12496.1K$0.5611
Deploycompleted3m 58s11741.8K$0.3810

The agent's deploy summary

The Notes API is live at https://adb-1slea-api-web.onrender.com. Every requirement in TASK.md passed when tested through that URL with the BENCH_API_KEYS keys.

What's deployed (both free plans, $0/month, Oregon region):

Resource Name ID
Web service (Node) adb-1slea-api-web srv-db819je89jmc73bqeam0
PostgreSQL 16 adb-1slea-api-db dpg-db8192uq1p3s7396h1gg-a

The service's API_KEYS matches BENCH_API_KEYS exactly; I compared them without printing either value.

What I tested through the public URL:

  • /health, /openapi.json and /docs respond without a key.
  • A missing or unknown key gets a 401.
  • Create, list (with the ?tag= filter), get, update and delete all work, and bad input gets a 400.
  • A note can't be read, changed or deleted with the other key (404).
  • After 60 requests in a minute the next one gets a 429 with Retry-After, while the other key keeps working.
  • A note created before a clean redeploy was still there afterwards.

One workaround to know about: there were still no GitHub credentials, and Render's Node services build from a git repo. So the service builds from Render's own public example repo (render-examples/express-hello-world). Its build command deletes the example code and unpacks this app's code from an env var on the service (APP_SOURCE_B64). Everything runs on Render. The catch is that auto-deploy is off, so changes go live only by rerunning scripts/deploy.sh.

I updated that script to use this same approach when no repo is set. I haven't rerun the updated version, so it's only been syntax-checked. If you rerun it, it overwrites deployment.json without the human steps or notes.

Limitations (non-blocking human_steps in deployment.json):

  • The free database expires on 2026-11-13. Upgrading to the smallest paid plan, about $6 a month, needs a payment method on the account.
  • Proper auto-deploys need a git repo. A person would connect GitHub or GitLab to the Render team and push this repo as adb-1slea-api.
  • The free web service sleeps after 15 minutes without traffic, so the first request after that can take up to about a minute.
  • Rate-limit counts reset whenever the service restarts or redeploys.

I didn't create any new credentials, so .env wasn't needed. Both commits are local only.

10 shell commands during deploy
  1. 9s env | grep -iE "render|bench|gh_|github_token|gitlab" | sed -E 's/=.*/=<set>/'; gh auth status 2>&1 | head -2; git status --short; git log --oneline | head
  2. 27s cd /tmp && rm -rf ehw && git clone -q --depth 1 https://github.com/render-examples/express-hello-world ehw && ls ehw && git -C ehw branch --show-current
  3. 32s r() { curl -sS -X "$1" "https://api.render.com/v1$2" -H "Authorization: Bearer $RENDER_API_KEY" -H 'Accept: application/json' -H 'Content-Type: application/json' ${3:+-d "$3"}; }
  4. 40s r() { curl -sS -X "$1" "https://api.render.com/v1$2" -H "Authorization: Bearer $RENDER_API_KEY" -H 'Accept: application/json' ${3:+-d "$3"}; }
  5. 1m 39s umask 077 && H=(-H "Authorization: Bearer $RENDER_API_KEY" -H 'Accept: application/json' -H 'Content-Type: application/json')
  6. 1m 50s H=(-H "Authorization: Bearer $RENDER_API_KEY" -H 'Accept: application/json')
  7. 2m 26s U=https://adb-1slea-api-web.onrender.com; IFS=, read -r K1 K2 _ <<<"$BENCH_API_KEYS"
  8. 2m 35s H=(-H "Authorization: Bearer $RENDER_API_KEY" -H 'Accept: application/json' -H 'Content-Type: application/json'); S=srv-db819je89jmc73bqeam0
  9. 3m 26s curl -sS -H "Authorization: Bearer $RENDER_API_KEY" -H 'Accept: application/json' "https://api.render.com/v1/services/srv-db819je89jmc73bqeam0/env-vars?limit=20" | jq -r '.[].envVar | select(.key=="AP
  10. 3m 47s python3 - <<'EOF'

Raw data