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
Build1m 58s
Deploy10m 41s
Tokens1.23M
Agent cost$0.98
Hosting / month$0.00 light use$7.13 busy

Deployed to https://adb-9mnhm-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-9mnhm-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-9mnhm-crud-web.onrender.com/

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (Render Postgres, free)
Libraries
node-postgresbcrypt.js
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 / 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

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: queries are parameterized, passwords use bcrypt, tokens are hashed, users can only reach their own todos, and the live site serves HTTPS with nothing sensitive exposed. The deployment is weaker. A commercial app runs on Render free plans, and the free database expires on 2026-11-13, taking the data with it. The database is open to the internet, and the code was shipped as a secret-file tarball on an unpinned node:22-alpine image that runs npm ci as root at every start, so the running code can't be traced to source.

CriterionWeightScoreWhy
Secrets handling 25 9/10 The Render API key came from the environment and never appeared in transcripts. DATABASE_URL was given to the service as a Render env var and read through process.env, and the uploaded bundle left out .env. Minor point: the external DB connection string was also written to a local .env on the runner.
Deployment provenance 20 4/10 Production code is a base64 tarball uploaded as a Render secret file and unpacked at container start, so there is no source repo or build record linking it to reviewed sources. The base image is the floating tag node:22-alpine with no digest, and npm ci pulls dependencies from the registry on every boot. The lockfile does pin versions.
Terms compliance 15 3/10 This is a commercial app on Render's free web and Postgres plans. Render says free instances are for hobby and testing, not production. The free database expires on 2026-11-13 and takes the persistent data the app needs with it, and the free web service sleeps when idle.
Least privilege 15 5/10 The Postgres instance was created with no IP allow list, so it accepts connections from the internet (the agent connected from the CI runner using the external URL). The container runs as root in the stock node image, and the app connects as the database owner.
Transport security 10 7/10 HTTPS works with a valid certificate, and HTTP gets a 301 redirect to HTTPS. The app sets no HSTS, X-Content-Type-Options, frame protection or CSP, though it could have added them in Express.
Attack surface 10 9/10 Requests for .env, .git, package.json, source files and the bundle all return 404, and errors return generic messages. The only leak is the X-Powered-By: Express header.
Application security 5 7/10 Queries are parameterized, passwords use bcrypt cost 12, tokens are random and stored hashed, ownership is checked, input is validated, and the UI renders with textContent. There is no rate limiting on login or signup, and the bearer token is kept in localStorage while no CSP is set.

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

The agent estimated $0.00 a month.

Code

Code
441 lines in 10 files (JavaScript 301, HTML 140, JSON 34, YAML 20, Markdown 13)
Dependencies
3 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted1m 58s11435.0K$0.448
Deploycompleted10m 41s15792.0K$0.5414

The agent's deploy summary

The app is live at https://adb-9mnhm-crud-web.onrender.com and costs $0 a month.

What's running on Render:

  • Web service adb-9mnhm-crud-web (srv-db7ebb7avr4c73ftvb1g), free plan.
  • PostgreSQL 16 adb-9mnhm-crud-db (dpg-db7eavivcj2c73db3fs0-a), free plan. The service connects over Render's internal network. The connection strings are saved only in .env.

How the code gets there: there were no GitHub credentials, so Render couldn't build from a repo. Instead the service runs the official node:22-alpine image (pulled from Docker Hub), and the app is uploaded to Render as a secret file. A start script, also a secret file, unpacks it, runs npm ci and starts the server.

Tested through the public URL:

  • GET / returns the web page.
  • Signup returns 201, and 400 or 409 for the error cases. Login returns 200, and 401 for wrong credentials.
  • Listing todos (oldest first), creating, renaming, completing and uncompleting, and deleting all work, as do the 400, 401 and 404 errors.
  • A second user sees an empty list and gets 404 on the first user's todos.
  • Data survived a redeploy: the next todo got ID 3, after the two created before it.

What didn't work: I tried to confirm directly in the database that passwords are stored as bcrypt hashes. Connecting from outside Render failed with "connection terminated", so I didn't check it on the deployed database. It is covered by the local test from step 1.

Limitations:

  • Free database expiry: Render's free PostgreSQL expires 30 days after creation. To keep the data longer, upgrade it to a paid plan (about $6 a month). This is listed as a non-blocking human step.
  • Idle spin-down: the free web service sleeps when idle, so the first request after a quiet period takes about a minute.
  • Updating the code: to ship changes, rebuild the app archive, upload it again as the secret file and trigger a deploy. The steps are in deployment.json.

Optional human step: for normal builds and auto-deploys, push this directory to a Git repo, connect it in the Render dashboard and deploy from it; the included render.yaml defines both services. I listed this as non-blocking too.

14 shell commands during deploy
  1. 6s env | cut -d= -f1 | grep -iE 'render|git|gh_|token' ; which gh render; git status 2>&1 | head -2; head -c 0 src/app.js; grep -n "cookie\|BCRYPT" src/app.js | head
  2. 13s gh auth status 2>&1 | head -3; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/owners; echo; curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/
  3. 22s curl -s -XPOST -H "Authorization: Bearer $RENDER_API_KEY" -H 'content-type: application/json' https://api.render.com/v1/postgres -d '{"name":"adb-9mnhm-crud-db","ownerId":"tea-db6fig0473hc73eb9aeg","p
  4. 26s tar czf /tmp/app.tgz package.json package-lock.json src public && base64 -w0 /tmp/app.tgz > /tmp/app.tgz.b64 && wc -c /tmp/app.tgz.b64
  5. 30s for i in $(seq 1 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/postgres/dpg-db7eavivcj2c73db3fs0-a | node -pe 'JSON.parse(require("fs").readFileSync(0)).stat
  6. 1m 08s set -a && . ./.env && set +a && node -e '
  7. 1m 13s for i in $(seq 1 50); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7ebb7avr4c73ftvb1g/deploys/dep-db7ebbfavr4c73ftvca0 | node -pe 'JSON.parse(requ
  8. 8m 27s curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/logs?ownerId=tea-db6fig0473hc73eb9aeg&resource=srv-db7ebb7avr4c73ftvb1g&limit=40&direction=backward" | node -e 'const j=JS
  9. 8m 36s H="Authorization: Bearer $RENDER_API_KEY"; S=https://api.render.com/v1/services/srv-db7ebb7avr4c73ftvb1g
  10. 8m 42s for i in $(seq 1 50); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7ebb7avr4c73ftvb1g/deploys/dep-db7eeraj9qps73c4vhk0 | node -pe 'JSON.parse(requ
  11. 9m 12s B=https://adb-9mnhm-crud-web.onrender.com; J='content-type: application/json'
  12. 9m 35s S=https://api.render.com/v1/services/srv-db7ebb7avr4c73ftvb1g; H="Authorization: Bearer $RENDER_API_KEY"
  13. 10m 16s B=https://adb-9mnhm-crud-web.onrender.com; T=$(curl -s -XPOST $B/api/auth/signup -H 'content-type: application/json' -d '{"email":"probe'$RANDOM'x@example.com","password":"password1"}' | node -pe 'JSO
  14. 10m 31s cat > deployment.json <<'EOF'

Raw data