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 & compliance69/100
Build2m 37s
Deploy11m 14s
Tokens1.30M
Agent cost$1.18
Hosting / month$0.00 light use$7.13 busy

Deployed to https://adb-drsgc-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-drsgc-api-web.onrender.com/docs

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (Render Postgres, free)
Libraries
node-postgres
Packaging
Render Blueprint (render.yaml)
Hosting
Render web service (free, running the prebuilt image node:22-alpine)

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

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 is well built: queries are parameterized, ownership is enforced per key, it's served over HTTPS with an HTTP redirect, nothing beyond the app is exposed, and secrets were passed through Render env vars without leaking. The main weakness is provenance: production runs a locally built bundle smuggled in as a base64 'secret file' on an unpinned node:22-alpine image, with no source repo or lockfile. In addition, the free Postgres database will expire with all its data on 2026-11-13, the database likely accepts external connections, and the container runs as root.

CriterionWeightScoreWhy
Secrets handling 25 9/10 DATABASE_URL and API_KEYS were passed to Render as service environment variables through the API, built from shell variables so they never appeared in commands or output, and the payload file was deleted afterwards. The only blemish is a local .env holding the external DB URL. It was written with umask 077 and is git-ignored.
Deployment provenance 20 4/10 Production runs an esbuild bundle built on the agent's machine and uploaded as a base64 'secret file'. That makes it opaque and impossible to trace to any source revision. It runs on the floating node:22-alpine tag, and dependencies were installed with caret ranges and no committed lockfile, using an unpinned `npx -y esbuild@0.25`.
Terms compliance 15 5/10 Render's free tier suits a personal hobby project, and sleeping after idle is tolerable for friends' scripts. But the free Postgres database expires on 2026-11-13 (30 days after creation), and its data goes with it. That breaks the requirement that notes survive over time. The agent disclosed this but didn't fix it.
Least privilege 15 6/10 The app connects to Postgres over Render's internal URL, but the database was created without an IP allow list. A full external connection string exists, so the database is probably reachable from the internet with a password. The container also runs as root in the stock node image, because the docker command never drops privileges.
Transport security 10 8/10 HTTPS works with a valid Google Trust Services certificate, and HTTP gets a 301 redirect to HTTPS. Neither the app nor the platform sends HSTS, X-Content-Type-Options, frame protection or CSP, and the app could have added them.
Attack surface 10 10/10 Probes of /.env, /.git/config, /package.json, /src/app.js, /admin and /debug all returned generic JSON 404s. Bad JSON returns a clean error with no stack trace. Only the documented endpoints are exposed.
Application security 5 8/10 All SQL is parameterized, input types are validated, ownership is enforced in every query (other keys get 404), and the docs HTML is escaped. The rate limit applies only after successful authentication, though, and note sizes and counts are only bounded by the 1 MB body limit.

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-drsgc-api-webfree$0.00free x 1 instance(s)
postgresadb-drsgc-api-dbfree$0.00Postgres free

The agent estimated $0.00 a month.

Code

Code
494 lines in 11 files (JavaScript 494, Markdown 35, JSON 30, YAML 22)
Dependencies
2 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 37s11458.9K$0.5310
Deploycompleted11m 14s14837.1K$0.6613

The agent's deploy summary

I deployed the Notes API to Render at https://adb-drsgc-api-web.onrender.com, and every requirement in TASK.md works through that URL. It costs $0/month and no human steps were needed.

Resources (both on the free plan):

  • Web service adb-drsgc-api-web (srv-db7ejht9fdbs73duqhf0) in Oregon, with a /health health check.
  • PostgreSQL 16 database adb-drsgc-api-db (dpg-db7ej2u7bikc73ajlu10-a). The service connects to it over Render's internal address.
  • API_KEYS is set to exactly the keys in BENCH_API_KEYS, which I checked against the service's settings.

How the code got onto Render: The Render account had no Git provider connected and there were no GitHub credentials, so Render couldn't build from a repo. Instead, the service runs the official public node:22-alpine image. The app is bundled into one JavaScript file and uploaded as a Render secret file, and a small start.sh secret file unpacks and runs it on start. My first deploy failed because Render passed the quotes in the start command through literally; moving the start logic into start.sh fixed it. To ship code changes, rebuild the bundle, re-upload it and redeploy; the steps are in the README.

Tested through the public URL:

  • /health, /openapi.json and /docs (HTML) respond.
  • 401 for a missing or unknown key.
  • Create, list (newest first), tag filter, get, update and delete all work, with 400s for bad input.
  • Other keys get 404 on notes they don't own.
  • 429 with Retry-After after 60 requests a minute, and one key hitting the limit doesn't affect another.
  • A note survived a full redeploy.

One check in my smoke script failed, but the mistake was in the script: it expected 6 path entries in the OpenAPI document, and there are correctly 5.

Limitations:

  • Database expiry: Render deletes free Postgres databases 30 days after creation unless they're upgraded (for example to basic-256mb, about $6/month). After that, the notes would be lost.
  • Cold starts: the free web service sleeps after about 15 minutes idle, so the first request after that is slow.
  • Rate limit counts: they're kept in memory on the single instance, so they reset whenever the service restarts.

The database connection strings and API keys are saved only in .env, which is git-ignored. deployment.json has the resource details and these notes.

13 shell commands during deploy
  1. 5s env | cut -d= -f1 | grep -vE '^(PATH|HOME|PWD|SHLVL|_)$' | sort | tr '\n' ' '; echo; gh auth status 2>&1 | head -5
  2. 16s A="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$A" https://api.render.com/v1/owners | head -c 800; echo; curl -s -H "$A" "https://api.render.com/v1/services?limit=20" | head -c 1500; echo; cur
  3. 27s mkdir -p /tmp/bundle && npx -y esbuild@0.25 src/server.js --bundle --platform=node --target=node22 --minify --external:pg-native --outfile=/tmp/bundle/app.js 2>&1 | tail -3 && gzip -9c /tmp/bundle/app
  4. 37s cd /tmp && docker run -d --name tpg -e POSTGRES_PASSWORD=pg -p 5433:5432 postgres:16 >/dev/null && sleep 4 && docker run -d --name tapp --network host -v /tmp/bundle:/etc/secrets:ro -e DATABASE_URL=po
  5. 56s A="Authorization: Bearer $RENDER_API_KEY"; curl -s -X POST -H "$A" -H content-type:application/json https://api.render.com/v1/postgres -d '{"name":"adb-drsgc-api-db","ownerId":"tea-db6fig0473hc73eb9ae
  6. 1m 00s A="Authorization: Bearer $RENDER_API_KEY"; for i in $(seq 1 40); do s=$(curl -s -H "$A" https://api.render.com/v1/postgres/dpg-db7ej2u7bikc73ajlu10-a | node -pe 'JSON.parse(require("fs").readFileSync(
  7. 1m 54s A="Authorization: Bearer $RENDER_API_KEY"
  8. 2m 01s A="Authorization: Bearer $RENDER_API_KEY"; for i in $(seq 1 40); do s=$(curl -s -H "$A" https://api.render.com/v1/services/srv-db7ejht9fdbs73duqhf0/deploys/dep-db7eji59fdbs73duqijg | node -pe 'JSON.pa
  9. 9m 16s A="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$A" "https://api.render.com/v1/logs?ownerId=tea-db6fig0473hc73eb9aeg&resource=srv-db7ejht9fdbs73duqhf0&limit=40&direction=backward" | node -e 'co
  10. 9m 32s A="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7ejht9fdbs73duqhf0
  11. 10m 12s cat > /tmp/smoke.mjs <<'EOF'
  12. 10m 26s A="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7ejht9fdbs73duqhf0; U=https://adb-drsgc-api-web.onrender.com; K=$(echo $BENCH_API_KEYS | cut -d, -f1)
  13. 11m 04s cat > deployment.json <<'EOF'

Raw data