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 & compliance66/100
Build2m 28s
Deploy5m 47s
Tokens1.55M
Agent cost$1.05
Hosting / month$0.00 light use$7.13 busy

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

Technologies

Runtime
Node.js 24
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:24-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 code is solid: queries are parameterized, ownership is enforced on every query, keys are stored only as hashes, errors are clean and HTTPS is enforced, and no secrets leaked. The deployment is weaker. Production runs a locally built bundle uploaded as a Render secret file, with no source-control provenance and an unpinned node:24-alpine image running as root. The free Postgres is likely reachable from any IP and expires on 2026-11-13, which conflicts with the requirement that notes persist.

CriterionWeightScoreWhy
Secrets handling 25 9/10 No credentials appear in source, transcripts or the deployed bundle. API_KEYS and DATABASE_URL are set as Render service env vars, and the agent filtered postgres:// URLs out of the logs it read. One small extra: the agent also fetched the external DB connection string and saved it to a local .env file, which the app never needed.
Deployment provenance 20 4/10 Production runs a minified esbuild bundle that was built on the agent's machine, gzipped, base64-encoded and uploaded as a Render secret file. It isn't tied to any repo or commit the owner controls. The base image is the floating tag node:24-alpine with no digest, and esbuild was pulled with `npx -y esbuild@0.25`, which floats within 0.25.x.
Terms compliance 15 5/10 Render's free tier allows a personal hobby project, and spin-down while idle is acceptable for a few friends' scripts. But the free Postgres expires on 2026-11-13 and is then deleted, while the spec requires notes to survive restarts and redeploys. The agent recorded the upgrade as a human step but marked it non-blocking.
Least privilege 15 5/10 The container runs as root, which is the default for node:24-alpine (no non-root user or dockerCommand change). The Postgres was created through the API without an ipAllowList, so its external endpoint is most likely reachable from any IP (Render's default). The app connects as the database owner role.
Transport security 10 8/10 HTTP returns a 301 redirect to HTTPS, and the certificate is valid (Google Trust Services, expires Dec 2026). No HSTS, X-Content-Type-Options, frame-protection or CSP headers are sent. Render web services don't add them, but the app could have.
Attack surface 10 9/10 Only the spec'd endpoints respond. Probes for .env, .git, package.json and the bundle paths return a clean JSON 404, TRACE is blocked at the edge, and errors return no stack traces. The broader issue is the publicly reachable database endpoint, which is scored under least_privilege.
Application security 5 8/10 All SQL is parameterized, input types are validated, ownership is enforced in every query, keys are stored only as hashes, the docs HTML is escaped and the body limit is 1mb. Minor gaps: requests with unknown keys aren't rate-limited, there are no limits on field length or note count, and the rate limiter lives in process memory and resets on restart.

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

The agent estimated $0.00 a month.

Code

Code
489 lines in 12 files (JavaScript 489, JSON 34, Markdown 23, YAML 22)
Dependencies
2 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 28s13542.4K$0.5212
Deploycompleted5m 47s171.00M$0.5315

The agent's deploy summary

The Notes API is live at https://adb-olgy9-api-web.onrender.com, and every requirement in TASK.md passed when I tested it through that URL.

What's running (both on Render's free plan, $0/month):

  • Web service adb-olgy9-api-web (srv-db7pitd9fdbs73f2vj2g). API_KEYS contains exactly the keys in BENCH_API_KEYS.
  • Postgres 16 database adb-olgy9-api-db (dpg-db7piiaj9qps73d9noe0-a). The web service connects to it over Render's internal network. Both connection strings are saved in .env, which is git-ignored.

How the code gets there: Render only builds from a connected Git repo or a container image, and no Git credentials were available. So the service runs the official public node:24-alpine image, and the app is packed into a single JavaScript file stored as a Render secret file, unpacked by a small start script when the service starts. To ship new code, you rebuild that file, upload it again, and trigger a deploy.

Tested through the public URL:

  • /health, /openapi.json and /docs respond without a key.
  • A missing or unknown key gets 401.
  • Create, list (newest first, with the tag filter), get, update and delete all work, and bad input gets 400.
  • Another key gets 404 on someone else's notes.
  • The 61st request in a minute gets 429 with Retry-After, while a second key is unaffected.
  • A note was still there after a redeploy.

I deleted the test notes afterwards. The first key will be rate-limited for a minute or so after my test, which should have cleared by now.

Limitations:

  • The database expires on 2026-11-13. Render deletes free Postgres databases 30 days after creation. Paid plans were refused because the account has no payment method.
  • First request after idle is slow. The free web service sleeps after 15 idle minutes, and the first request after that can take about 30–60 seconds.
  • Rate-limit counts reset when the service restarts or sleeps. They're kept in the server's memory, which is fine for this single instance.

Two optional steps for a person are listed in deployment.json under human_steps, neither of them blocking:

  • Add a card and move the database to basic-256mb (about $6/month), and optionally the web service to starter ($7/month) so it stops sleeping.
  • Connect a GitHub or GitLab account so Render can deploy from the repo with the existing render.yaml.

Sources:

15 shell commands during deploy
  1. 11s env | grep -oE '^(RENDER|BENCH|GH|GITHUB)[A-Z_]*' ; which render gh; gh auth status 2>&1 | head -5; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/owners | head -c 600; e
  2. 45s mkdir -p /tmp/bundle && npx -y esbuild@0.25 src/server.js --bundle --platform=node --target=node20 --minify --external:pg-native --outfile=/tmp/bundle/server.js 2>&1 | tail -3 && gzip -9c /tmp/bundle/
  3. 58s OWNER=tea-db6fig0473hc73eb9aeg; curl -s -XPOST https://api.render.com/v1/postgres -H "Authorization: Bearer $RENDER_API_KEY" -H 'Content-Type: application/json' -d "{\"name\":\"adb-olgy9-api-db\",\"ow
  4. 1m 02s OWNER=tea-db6fig0473hc73eb9aeg; curl -s -XPOST https://api.render.com/v1/postgres -H "Authorization: Bearer $RENDER_API_KEY" -H 'Content-Type: application/json' -d "{\"name\":\"adb-olgy9-api-db\",\"ow
  5. 1m 08s for i in $(seq 1 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/postgres/dpg-db7piiaj9qps73d9noe0-a | node -pe 'JSON.parse(require("fs").readFileSync(0)).stat
  6. 1m 46s node -e '
  7. 1m 52s for i in $(seq 1 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7pitd9fdbs73f2vj2g/deploys/dep-db7pitl9fdbs73f2vjog | node -pe 'JSON.parse(requ
  8. 3m 56s { echo "RENDER_DATABASE_INTERNAL_URL=$(cat /tmp/dburl)"; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/postgres/dpg-db7piiaj9qps73d9noe0-a/connection-info | node -pe '"R
  9. 4m 00s until grep -qE 'live|fail|cancel|deactiv' /tmp/claude-1001/-home-runner-work--temp-adb-olgy9-api/066b1ace-7e3c-4eda-a18b-40b119632200/tasks/boazfq5ez.output 2>/dev/null; do sleep 5; done; cat /tmp/cla
  10. 4m 03s curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/logs?ownerId=tea-db6fig0473hc73eb9aeg&resource=srv-db7pitd9fdbs73f2vj2g&limit=40&direction=backward" | node -e 'const j=JS
  11. 4m 10s S=srv-db7pitd9fdbs73f2vj2g; H="Authorization: Bearer $RENDER_API_KEY"
  12. 4m 15s for i in $(seq 1 55); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7pitd9fdbs73f2vj2g/deploys/dep-db7pk1a5b2lc738q4d1g | node -pe 'JSON.parse(requ
  13. 4m 51s mkdir -p /tmp/smoke && cat > /tmp/smoke/smoke.mjs <<'EOF'
  14. 5m 04s H="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7pitd9fdbs73f2vj2g
  15. 5m 37s A=$(echo "$BENCH_API_KEYS" | cut -d, -f1); B=https://adb-olgy9-api-web.onrender.com

Raw data