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
Build2m 00s
Deploy4m 14s
Tokens1.25M
Agent cost$0.88
Hosting / month$0.00 light use$7.13 busy

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

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (Render Postgres, free)
Libraries
node-postgresbcrypt.jscookie-parser
Dev tools
esbuild
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, session tokens are stored hashed, ownership is checked on every query, and the live site exposes no files or stack traces. Secrets also stayed out of logs and source. The weak spots are the deployment and hosting. The code runs as an opaque, locally built bundle uploaded as Render 'secret files' on an unpinned public node image as root, and the free Postgres is open to external access. A commercial app is hosted on Render's free plans, whose database expires on 2026-11-13 and whose web service sleeps when idle.

CriterionWeightScoreWhy
Secrets handling 25 9/10 The Render API key was only referenced as $RENDER_API_KEY and the DB connection string was moved between files by Python without being printed. DATABASE_URL is set as a Render service env var (the platform's secret store) and nothing secret is in the source. Minor: the external DB URL was also written to a local .env (git-ignored, chmod 600).
Deployment provenance 20 4/10 Production runs a minified esbuild bundle built on the agent's machine and uploaded as Render 'secret files' on top of a public node:22-alpine image. It is not tied to any repo or commit, can't be inspected in the dashboard, and the base image is pinned by tag only, not digest. The lockfile does pin the dependencies that went into the bundle.
Terms compliance 15 3/10 This is a business (commercial) app on Render's free web and Postgres plans. Render presents these for hobby and testing use: the web service sleeps when idle, and the free database expires on 2026-11-13, 30 days after creation. A business's persistent todo store therefore can't rely on it. As far as I know, Render's terms don't outright ban commercial use of the free tier, but its expiry and sleep limits rule it out for this use.
Least privilege 15 5/10 The app uses the internal DB connection string, but the free Postgres was created via the API with the default external access left open to any IP (no ipAllowList restriction). The container runs node:22-alpine with its default root user, and the app connects as the database owner role.
Transport security 10 7/10 The app is served over HTTPS with a valid Render/Cloudflare certificate, and HTTP gets a 301 redirect to HTTPS. HSTS, CSP, X-Content-Type-Options and frame protection are all missing, though the app could easily set them (e.g. helmet). X-Powered-By: Express is also sent.
Attack surface 10 9/10 Requests for /.env, /.git/config, /package.json, source and bundle paths all return 404, there's no directory listing, and errors return generic JSON without stack traces. The only minor leaks are the X-Powered-By header and Express's default HTML 404 page.
Application security 5 7/10 Every query is parameterized, passwords use bcrypt cost 12, session tokens are random and stored only as SHA-256 hashes, ownership is enforced in the SQL WHERE clause, and the UI renders with textContent. But login and signup have no rate limiting (and bcrypt signup is CPU-heavy), and the UI keeps the bearer token 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-gsxkv-crud-webfree$0.00free x 1 instance(s)
postgresadb-gsxkv-crud-dbfree$0.00Postgres free

The agent estimated $0.00 a month.

Code

Code
416 lines in 11 files (JavaScript 280, HTML 136, JSON 37, Markdown 20, YAML 20)
Dependencies
5 npm, 0 Python
Deploy config
render.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 00s10397.1K$0.429
Deploycompleted4m 14s16855.9K$0.4615

The agent's deploy summary

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

What I created on Render (both free, $0/month):

  • adb-gsxkv-crud-db: PostgreSQL 16 database, id dpg-db7pdq2vcj2c73efd6o0-a.
  • adb-gsxkv-crud-web: web service, id srv-db7pe6mq1p3s73da62b0. It connects to the database over Render's internal network.

How the code got there: there were no GitHub credentials, so Render couldn't pull from a repo. Instead, the service runs the public node:22-alpine image. I bundled the app into one compressed file and uploaded it, along with the HTML page and a small startup script, as Render "secret files" through the API. The service starts with node /etc/secrets/boot.js. My first attempt used a shell one-liner as the start command and failed, because Render passed the quoted string as a single word; the startup script replaced it.

What I tested through the public URL:

  • Signup and login: 400 for a bad email or a short password, 409 for a duplicate email, 401 for wrong credentials, and 201/200 with a token on success.
  • Todos: create, list oldest first, rename, complete and uncomplete, and delete all return the right status codes. Requests without a token get 401, and an empty title gets 400.
  • Isolation: a second user sees an empty list and gets 404 when trying to edit or delete the first user's todo.
  • Persistence: after a redeploy, the todos were still there and the same login token still worked.
  • Web page: a headless Chromium run through sign up, add, complete, rename, uncomplete, delete and log out worked end to end.

Free-tier limits:

  • The web service sleeps when idle, so the first request after a quiet spell takes about 30–60 seconds.
  • Render deletes free Postgres databases 30 days after creation. If the data needs to last longer, upgrade it (Basic-256mb is about $6/month).

Updating the code later: run npm run bundle, upload the secret files again and trigger a deploy. render.yaml remains for a git-based deploy if a repo is connected later.

The database connection strings are saved in .env, which is git-ignored. No secrets were printed or written to committed files. deployment.json is written, with no human steps needed.

15 shell commands during deploy
  1. 6s cd /home/runner/work/_temp/adb-gsxkv-crud; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/owners | head -c 600; echo; curl -s -H "Authorization: Bearer $RENDER_API_KEY" "
  2. 22s grep -n "static" src/app.js
  3. 28s python3 - <<'EOF'
  4. 37s gzip -9c dist/server.js | base64 -w0 > dist/server.js.gz.b64 && ls -la dist && docker run -d --name pg -e POSTGRES_PASSWORD=pg --network host postgres:16 >/dev/null && sleep 4 && mkdir -p /tmp/secrets
  5. 54s docker rm -f app pg >/dev/null; cd /home/runner/work/_temp/adb-gsxkv-crud
  6. 1m 04s for i in $(seq 1 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/postgres/dpg-db7pdq2vcj2c73efd6o0-a | python3 -c 'import json,sys;print(json.load(sys.stdin)["
  7. 1m 44s python3 - <<'EOF' > /tmp/svc_out.json
  8. 1m 50s for i in $(seq 1 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7pe6mq1p3s73da62b0/deploys/dep-db7pe76q1p3s73da638g | python3 -c 'import json,s
  9. 2m 03s curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/logs?ownerId=tea-db6fig0473hc73eb9aeg&resource=srv-db7pe6mq1p3s73da62b0&limit=30&direction=backward" | python3 -c 'import
  10. 2m 16s mkdir -p deploy && cat > deploy/boot.js <<'EOF'
  11. 2m 20s for i in $(seq 1 40); do s=$(curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/srv-db7pe6mq1p3s73da62b0/deploys/dep-db7peenavr4c7393dvpg | python3 -c 'import json,s
  12. 2m 53s B=https://adb-gsxkv-crud-web.onrender.com; J='Content-Type: application/json'; E="u$(date +%s)@example.com"
  13. 3m 12s D=$(curl -s -XPOST -H "Authorization: Bearer $RENDER_API_KEY" -H 'Content-Type: application/json' -d '{}' https://api.render.com/v1/services/srv-db7pe6mq1p3s73da62b0/deploys | python3 -c 'import json,
  14. 3m 47s mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i playwright >/dev/null 2>&1 && npx playwright install chromium >/dev/null 2>&1; cat > ui.js <<'EOF'
  15. 4m 05s python3 - <<'EOF'

Raw data