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

Database-Backed CRUD App on Render

Success

Checks passed19/19
Human steps25 min of a person's time
Security & compliance62/100
Stack fit84/100
Build2m 26s
Deploy4m 05s
Tokens1.29M
Agent cost$1.01
Hosting / month$0.00 light use$7.13 busy

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

Screenshots

The web app before signing in
The web app before signing in · https://adb-v6xkp-crud-web.onrender.com/
The todo list after signing in through the web UI as the test user
The todo list after signing in through the web UI as the test user · https://adb-v6xkp-crud-web.onrender.com/

Technologies

Runtime
Node.js 20+
Frameworks
Express
Data
PostgreSQL (Render Postgres, free)
Libraries
node-postgresbcrypt.js
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 / 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 / serves the web app as HTML ui
passed POST /api/auth/signup creates an account and returns a token signup
passed Signup rejects an invalid email and a short password with 400 signup_validation
passed Signing up twice with the same email returns 409 signup_duplicate
passed POST /api/auth/login returns a token login
passed Login with a wrong password returns 401 login_wrong_password
passed /api/todos requires a valid token (401) auth_required
passed POST /api/todos creates a todo create
passed An empty title is rejected with 400 create_validation
passed GET /api/todos lists the caller's todos list
passed Todos are listed oldest first list_order optional
passed PATCH /api/todos/{id} renames and completes a todo update
passed Users can't see or modify each other's todos (404) isolation
passed DELETE /api/todos/{id} removes a todo delete
passed Data is still there a minute later, with a fresh login persistence
passed The web UI renders a login or signup form browser_ui optional

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 managed Postgres is a well-reasoned, proportionate stack that suits Render. The only departure is the delivery workaround, which bypasses Render's normal git-backed deploys.

CriterionWeightScoreWhy
Fit for the job 40 9/10 Express with managed PostgreSQL via pg, bcryptjs and a single vanilla-JS page meets every requirement, and Postgres is a durable store for users and todos. The free plan's expiry is a hosting-plan problem, not a stack problem.
Proportion 30 9/10 One service, one database and three runtime dependencies, with no needless frameworks or stand-ins. That is about as lean as this task allows.
Platform fit 30 7/10 Render's native Node runtime plus managed Postgres on the internal network is the idiomatic Render setup, as STACK.md reasons. The actual deploy departs from the planned render.yaml/git flow by hijacking an example repo and shipping code through an env var, which works against Render's git-based deploys.

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 app code is solid: parameterized SQL, bcrypt, hashed session tokens, owner-scoped queries and no exposed files or secrets. The deployment is the problem. Production builds from Render's third-party express-hello-world repo (unpinned main), with the real code injected as a base64 tarball in an env var, so there is no owned, versioned source. A commercial app also runs on Render's free tier, which sleeps when idle and has a Postgres that expires on 2026-11-13. Smaller gaps are an unrestricted external DB endpoint, missing security headers and no rate limiting on auth.

CriterionWeightScoreWhy
Secrets handling 25 9/10 The database URL and Render API key were read from the environment and never printed. The app gets DATABASE_URL from Render service env vars and no secrets are in the code bundle. One minor point: the connection string was copied in as a fixed value instead of linked with fromDatabase, so a credential rotation would not reach the service.
Deployment provenance 20 3/10 Production builds from a third-party public repo the owner doesn't control (render-examples/express-hello-world, branch main, not pinned to a commit). The real code arrives as a base64 tarball stuffed into an env var, so no owned, versioned source backs what runs. Dependencies are locked and installed with npm ci, but the Node version floats on major 22.
Terms compliance 15 3/10 This is a commercial app running entirely on Render's free tier. Render positions free instances for hobby and testing, not production (I'm fairly but not fully sure the terms don't outright ban commercial use). The free web service sleeps when idle, and the free Postgres expires on 2026-11-13, after which its data is deleted, so it breaks the task's durable-storage requirement.
Least privilege 15 6/10 The app connects to Postgres over Render's private network, which is good. But no ipAllowList was set on the database, so Render's default leaves its external endpoint reachable from any IP. Render's native runtime gives no control over the process user.
Transport security 10 7/10 The site is HTTPS-only with a valid certificate, and HTTP gets a 301 to HTTPS. The app sets none of HSTS, X-Content-Type-Options, frame protection or CSP, although Express could easily add them.
Attack surface 10 9/10 No source files, .env, .git or package files are served, and API errors are generic JSON with no stack traces. Express's default 'Cannot GET' HTML 404 pages reveal the framework, a minor leak.
Application security 5 7/10 Strong basics: parameterized SQL, bcrypt cost 12, random session tokens stored hashed, equal timing for unknown emails, owner-scoped queries that return 404, validated input types and lengths, and textContent rendering. However, login and signup have no rate limiting, which allows credential guessing and CPU exhaustion through bcrypt, and the bearer token sits in localStorage with no CSP.

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

The agent estimated $0.00 a month.

Code

Code
507 lines in 14 files (JavaScript 285, HTML 138, Shell 84, Markdown 40, JSON 32, YAML 20)
Dependencies
3 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 26s9367.3K$0.468
Deploycompleted4m 05s10610.8K$0.319

The agent's deploy summary

The app is live at https://adb-v6xkp-crud-web.onrender.com, and every TASK.md requirement passes through that URL. Nothing needed a person, so human_steps is empty.

What's deployed (all free tier, $0/month, Oregon region):

  • Web service adb-v6xkp-crud-web (srv-db813tgn74is73dcj200): runs the Node 22 / Express app, with /healthz as its health check.
  • PostgreSQL 16 database adb-v6xkp-crud-db (dpg-db8139psrm7s738ak9q0-a): the app connects to it over Render's internal network.

Tested through the public URL:

  • Smoke test: all 20 checks in scripts/smoke.sh pass. They cover the HTML page, every status code in the spec, oldest-first ordering, rename, complete/uncomplete, delete, and 404s when one user touches another user's todos.
  • Persistence: a user and a todo I created were still there, and the login still worked, after a redeploy with the build cache cleared.
  • HTTPS: plain HTTP redirects to HTTPS.

The unusual part: there were still no GitHub credentials, and Render web services build from a Git repo. So the service points at Render's own public example repo (render-examples/express-hello-world). Its build command deletes that code and unpacks this app from a base64 tarball stored in the service's APP_BUNDLE env var. All code runs on Render, and no outside host or registry is involved. The catch is that automatic deploys are off: to ship a change, run scripts/update-bundle.sh. If the code is later pushed to a real repo, point the service at it; render.yaml describes the same setup.

Free-tier limits:

  • The web service goes to sleep after about 15 minutes without traffic, so the next request can take up to about a minute.
  • The free database expires 30 days after it was created (today). To keep the data longer, upgrade it to the paid plan, about $6/month.

The API key was only read from the environment and was never printed or saved, and I didn't create any new credentials, so .env wasn't needed. Everything is committed in the local git repo; deployment.json has the details.

9 shell commands during deploy
  1. 6s env | grep -iE 'render|git|token' | sed 's/=.*/=<set>/'; gh auth status 2>&1|head -2; H="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$H" https://api.render.com/v1/owners; echo; curl -s -H "$H"
  2. 21s H="Authorization: Bearer $RENDER_API_KEY"; curl -s -H "$H" -H 'Content-Type: application/json' -X POST https://api.render.com/v1/postgres -d '{"name":"adb-v6xkp-crud-db","ownerId":"tea-db6fig0473hc73e
  3. 25s git ls-remote https://github.com/render-examples/express-hello-world HEAD refs/heads/main | head; tar czf /tmp/app.tgz app.js db.js server.js package.json package-lock.json public/index.html && base64
  4. 31s H="Authorization: Bearer $RENDER_API_KEY"; for i in $(seq 40); do s=$(curl -s -H "$H" https://api.render.com/v1/postgres/dpg-db8139psrm7s738ak9q0-a | python3 -c 'import json,sys;print(json.load(sys.st
  5. 1m 40s H="Authorization: Bearer $RENDER_API_KEY"
  6. 1m 45s H="Authorization: Bearer $RENDER_API_KEY"; for i in $(seq 60); do s=$(curl -s -H "$H" https://api.render.com/v1/services/srv-db813tgn74is73dcj200/deploys/dep-db813ton74is73dcj2r0 | python3 -c 'import
  7. 2m 30s U=https://adb-v6xkp-crud-web.onrender.com; curl -s $U/healthz; echo; curl -s $U/ | grep -o '<title>.*</title>'; scripts/smoke.sh $U; echo exit=$?; curl -sI http://adb-v6xkp-crud-web.onrender.com/ | he
  8. 2m 50s U=https://adb-v6xkp-crud-web.onrender.com; H="Authorization: Bearer $RENDER_API_KEY"; E="persist$RANDOM@example.com"
  9. 3m 57s cat > scripts/update-bundle.sh <<'EOF'

Raw data