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 & compliance60/100
Build2m 06s
Deploy7m 07s
Tokens1.28M
Agent cost$0.89
Hosting / month$0.00 light use$7.13 busy

Deployed to https://adb-4f010-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-4f010-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-4f010-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: parameterized SQL, bcrypt, hashed opaque tokens, correct per-user authorization, and nothing sensitive exposed over a properly redirected HTTPS endpoint. The deployment is the weak point. The code is smuggled in as a base64 env var, eval'd at boot on an unpinned node:22-alpine image, and runs `npm ci` on every start, so it has no traceable provenance. The container runs as root, and the database's external access was left open. Most significantly, this commercial app runs entirely on Render's free tier, where the web service sleeps and the free Postgres holding the business's data expires on 2026-11-12.

CriterionWeightScoreWhy
Secrets handling 25 8/10 The Render API key stayed in the environment and was never printed. DATABASE_URL (the internal connection string) was injected through Render service env vars, and the DB URLs written to a local .env (chmod 600, gitignored) were kept out of the source tarball. Minor issues: a hardcoded fallback dev credential in db.js, and the agent saved the external DB connection string when it didn't need it.
Deployment provenance 20 4/10 Production code is not built from a source the owner controls. A base64 tarball is stuffed into an env var, then unpacked and eval'd at boot via `node -e eval(process.env.BOOT_JS)` on the mutable `node:22-alpine` tag, and `npm ci` runs against the public registry on every start. The lockfile pins dependency integrity, but there is no traceable commit or immutable artifact, and render.yaml does not match what was deployed.
Terms compliance 15 3/10 This is a commercial team app hosted entirely on Render's free tier. Free web services spin down when idle, and the free Postgres the app relies on for persistence expires on 2026-11-12, about 30 days after creation, which will take the business's data and app offline. To my knowledge Render doesn't explicitly forbid commercial use on the free plan, but it positions the plan for hobby and testing, not production.
Least privilege 15 5/10 The app reaches Postgres over Render's internal network, but the agent never restricted the database's external access (the default IP allow list is open; its external hostname was reachable from the agent's runner). The container runs as root in the stock node image.
Transport security 10 7/10 HTTPS uses a valid Google Trust Services certificate and HTTP 301-redirects to HTTPS. HTML responses lack HSTS, CSP, X-Content-Type-Options and frame protection, which the app could have set (e.g. with helmet).
Attack surface 10 9/10 Only public/ is served statically. Probes of .env, .git, package.json, src files and directory paths all returned 404, and the error handler hides stack traces. Only a benign /healthz endpoint is extra.
Application security 5 7/10 Queries are parameterized, passwords use bcrypt with cost 12, and tokens are random 32-byte values stored as SHA-256 hashes. Ownership checks return 404 and the UI renders text with textContent. Minor gaps: no rate limiting on login/signup (brute force and bcrypt CPU abuse), sessions never expire, and the token is kept in localStorage.

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

The agent estimated $0.00 a month.

Code

Code
373 lines in 10 files (JavaScript 249, HTML 124, JSON 28, YAML 20, Markdown 18)
Dependencies
3 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 06s10395.1K$0.429
Deploycompleted7m 07s16889.1K$0.4714

The agent's deploy summary

The app is live at https://adb-4f010-crud-web.onrender.com, and every requirement in TASK.md works through that URL.

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

  • adb-4f010-crud-db: a Render-managed PostgreSQL 16 database. The app connects to it over Render's internal network. Both connection strings are saved in .env only.
  • adb-4f010-crud-web: the web service, with a health check at /healthz.

How the code got there: there were no GitHub credentials, so Render had no repo to build from. Instead the service runs the public node:22-alpine image, and the app source is passed in as a base64 tarball in an environment variable (APP_TGZ_B64). Each time it starts, it unpacks the code, runs npm ci and starts the server. Updating the code means rebuilding that tarball and redeploying. Hooking up a Git repo would give normal builds instead; I listed that in deployment.json as an optional human step, not a blocking one.

What I tested on the public URL:

  • API: every endpoint and every error code (400, 401, 404, 409).
  • Isolation: another user's todo returns 404 for both edit and delete.
  • Persistence: a todo I created was still there after restarting the service.
  • Web UI: a headless-browser run of signup, create, rename, complete and uncomplete, delete, and logout.

Limitations:

  • Database expiry: Render's free Postgres expires 30 days after creation unless it's upgraded to a paid plan (for example basic-256mb, about $6/month).
  • Slow first request: the free web service sleeps when idle, so the first request afterwards takes about 30–60 seconds.
  • Slower starts: each start spends about 15 seconds installing dependencies.

My first deploy attempt failed because Render didn't run the shell-style start command as I'd written it. The second attempt used a different start command and worked.

deployment.json has the resource IDs, costs and these notes.

14 shell commands during deploy
  1. 5s env | grep -iE 'github|gh_|render' | sed 's/=.*/=***/'; gh auth status 2>&1 | head -5; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/owners | python3 -m json.tool | head
  2. 30s tar czf /tmp/app.tgz package.json package-lock.json src public && wc -c /tmp/app.tgz && base64 -w0 /tmp/app.tgz | wc -c
  3. 34s H=(-H "Authorization: Bearer $RENDER_API_KEY" -H "Content-Type: application/json")
  4. 39s H=(-H "Authorization: Bearer $RENDER_API_KEY")
  5. 1m 26s set -a && . ./.env && set +a && python3 - <<'EOF' > /tmp/svc.json
  6. 1m 31s H=(-H "Authorization: Bearer $RENDER_API_KEY")
  7. 2m 45s H=(-H "Authorization: Bearer $RENDER_API_KEY")
  8. 2m 59s python3 - <<'EOF' > /tmp/patch.json
  9. 3m 04s H=(-H "Authorization: Bearer $RENDER_API_KEY")
  10. 3m 34s B=https://adb-4f010-crud-web.onrender.com; J='content-type: application/json'
  11. 3m 54s H=(-H "Authorization: Bearer $RENDER_API_KEY"); S=https://api.render.com/v1/services/srv-db7bh7navr4c73fja9dg
  12. 5m 02s H=(-H "Authorization: Bearer $RENDER_API_KEY"); S=https://api.render.com/v1/services/srv-db7bh7navr4c73fja9dg; B=https://adb-4f010-crud-web.onrender.com; J='content-type: application/json'
  13. 6m 29s mkdir -p /tmp/uitest && cd /tmp/uitest && npm init -y >/dev/null && npm i playwright@1 >/dev/null 2>&1 && npx playwright install chromium >/dev/null 2>&1 && cat > t.js <<'EOF'
  14. 6m 59s cat > deployment.json <<'EOF'

Raw data