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 & compliance72/100
Build2m 20s
Deploy4m 52s
Tokens1.15M
Agent cost$0.93
Hosting / month$0.00 light use$7.13 busy

Deployed to https://adb-kpiad-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-kpiad-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 on Render keeps secrets in platform env vars, enforces per-key ownership with parameterized queries, serves only HTTPS and exposes nothing beyond its documented endpoints. Its main weakness is deployment provenance: the app source is stuffed into an env var and eval'd at boot on an unpinned node:22-alpine image, with npm ci running at runtime. Other problems: the free Postgres expires on 2026-11-12, which breaks the durable-storage requirement; the database's external endpoint is left open; the container runs as root; and no security headers are set.

CriterionWeightScoreWhy
Secrets handling 25 9/10 API_KEYS and DATABASE_URL are set as Render service env vars and read from process.env. No secret shows up in source, the committed repo, logs or the deployed tarball. The DB connection strings were written to a local .env, which is git-ignored. One minor point: the env-var store is also used to hold the entire app bundle.
Deployment provenance 20 5/10 The running code is the agent's own git archive. It is shipped base64-encoded in an env var and eval'd by a `node -e eval(process.env.BOOTJS)` start command. There is no image build, no Git source and no deploy record tied to a commit. The base image is the floating tag node:22-alpine with no digest. `npm ci` runs against the public registry on every boot, which is lockfile-pinned but fetched at runtime.
Terms compliance 15 6/10 Render's free tier allows personal hobby use, and a cold-starting web service is acceptable for a few friends' scripts. However, the free Postgres expires on 2026-11-12, 30 days after creation, and its data is then deleted. That breaks the spec's requirement that notes survive. The agent disclosed this but left it as is.
Least privilege 15 6/10 The app uses the internal DB URL. But Render Postgres was created with default access settings, so its external endpoint is reachable from the internet with no IP allow-list. The container runs as root: the node image default, with no USER switch in the start command. The app connects as the DB owner role, and the SSL fallback disables certificate verification.
Transport security 10 8/10 HTTPS is served with a valid Google Trust Services certificate, and HTTP gets a 301 redirect to HTTPS. No security headers are sent at all: no HSTS, X-Content-Type-Options, X-Frame-Options or CSP. The app could have set these and didn't.
Attack surface 10 9/10 Probes for /.env, /.git/config, /package.json, /src/server.js, /admin and /debug all returned a JSON 404. Errors are generic and leak no stack traces. Only the documented public endpoints exist. The 404 body echoes the request path, which is harmless here because it is sent as JSON.
Application security 5 8/10 Queries are parameterized and ownership is enforced in every query (owner_key = $2). Input types are validated, the body is limited to 1 MB, docs HTML is escaped and per-key rate limiting works. Minor gaps: there is no rate limit on unauthenticated or invalid-key requests, so key guessing is unthrottled. There are no length limits on title, content or tags beyond the 1 MB body. The API key is compared with a plain Set lookup.

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

The agent estimated $0.00 a month.

Code

Code
458 lines in 12 files (JavaScript 447, JSON 31, YAML 24, Markdown 22, Shell 11)
Dependencies
2 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 20s8328.9K$0.477
Deploycompleted4m 52s15817.3K$0.4614

The agent's deploy summary

The Notes API is live on Render at https://adb-kpiad-api-web.onrender.com, on free plans with an estimated cost of $0/month. Nothing needed a person, so human_steps is empty.

What's deployed:

  • adb-kpiad-api-web (srv-db7bnjvavr4c73fk27b0): a free web service with HTTPS on the onrender.com domain.
  • adb-kpiad-api-db (dpg-db7bn0jbc2fs73ej8c90-a): a free PostgreSQL 16 database. The app connects to it over Render's private network.
  • API_KEYS contains exactly the keys from BENCH_API_KEYS. The database connection strings are saved in .env, which is not committed.

Tests through the public URL: every check passed, using both benchmark keys:

  • /health, /openapi.json and /docs respond without a key.
  • Missing or unknown keys get 401.
  • Create, list, get, update and delete all work; bad input gets 400.
  • One key can't see or delete another key's notes (404).
  • Lists are newest first, and the tag filter works.
  • After 60 requests in a minute a key gets 429 with Retry-After, while the other key keeps working.
  • A note created before a redeploy was still there afterwards.

How the code gets to Render: the workspace has no GitHub connection and I had no GitHub credentials, so a normal Git-based deploy wasn't possible. Instead, the service runs the official public node:22-alpine image. The app source is stored as a compressed bundle in an environment variable, and the start command unpacks it, installs dependencies and starts the server. scripts/render-push.sh redeploys the committed code this way. If a repo is connected later, the render.yaml from step 1 can be used for a normal deploy.

The first two deploys failed because Render splits the start command on spaces and ignores quotes. A start command with no spaces fixed it.

Limitations of the free setup:

  • Cold starts: the web service sleeps after about 15 minutes idle, and the next request takes roughly 30–60 seconds.
  • Database expiry: the free database expires 30 days after creation. Upgrading to the basic-256mb plan (about $6/month) keeps it.
  • Rate-limit counters: they are kept in the app's memory on a single instance, so they reset whenever the service restarts or wakes from sleep.

All of this is recorded in deployment.json.

14 shell commands during deploy
  1. 7s env | grep -iE '^(GH_|GITHUB_TOKEN|RENDER|BENCH)' | sed 's/=.*/=<set>/'; gh auth status 2>&1 | head -3; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/owners | head -c 60
  2. 19s H="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$H" "https://api.render.com/v1/services?limit=50" | head -c 1500; echo; curl -s -H "$H" "https://api.render.com/v1/postgres?limit=50" | head -c 1
  3. 23s H="Authorization: Bearer $RENDER_API_KEY"; curl -s -XPOST -H "$H" -H 'content-type: application/json' https://api.render.com/v1/postgres -d '{"name":"adb-kpiad-api-db","ownerId":"tea-db6fig0473hc73eb9
  4. 30s git archive --format=tar.gz HEAD src package.json package-lock.json -o /tmp/app.tgz && ls -l /tmp/app.tgz && base64 -w0 /tmp/app.tgz | wc -c; git check-ignore .env && echo ignored
  5. 32s H="Authorization: Bearer $RENDER_API_KEY"; sleep 60; curl -s -H "$H" https://api.render.com/v1/postgres/dpg-db7bn0jbc2fs73ej8c90-a | node -pe 'JSON.parse(require("fs").readFileSync(0)).status'
  6. 1m 40s H="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$H" https://api.render.com/v1/postgres/dpg-db7bn0jbc2fs73ej8c90-a/connection-info > /tmp/ci.json
  7. 1m 45s H="Authorization: Bearer $RENDER_API_KEY"; for i in $(seq 30); do s=$(curl -s -H "$H" https://api.render.com/v1/services/srv-db7bnjvavr4c73fk27b0/deploys/dep-db7bnk7avr4c73fk27v0 | node -pe 'JSON.pars
  8. 2m 03s H="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$H" "https://api.render.com/v1/logs?ownerId=tea-db6fig0473hc73eb9aeg&resource=srv-db7bnjvavr4c73fk27b0&limit=40&direction=backward" | node -e 'co
  9. 2m 14s H="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7bnjvavr4c73fk27b0; curl -s -XPATCH -H "$H" -H 'content-type: application/json' https://api.render.com/v1/services/$S -d '{"serviceDetails":{"envSpec
  10. 2m 44s H="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7bnjvavr4c73fk27b0
  11. 3m 43s cat > /tmp/remote.mjs <<'EOF'
  12. 3m 54s sleep 61; H="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7bnjvavr4c73fk27b0; K=${BENCH_API_KEYS%%,*}; U=https://adb-kpiad-api-web.onrender.com
  13. 3m 59s H="Authorization: Bearer $RENDER_API_KEY"; S=srv-db7bnjvavr4c73fk27b0; K=${BENCH_API_KEYS##*,}; U=https://adb-kpiad-api-web.onrender.com
  14. 4m 43s mkdir -p scripts && cat > scripts/render-push.sh <<'EOF'

Raw data