Render · Opus 5.5 · medium effort · HIPAA organization · trial 1
Static Content Site on Render
Success
Deployed to https://adb-t3gz2-static-web.onrender.com. Every deployment is deleted after it's verified and reviewed.
Screenshots
Technologies
- Runtime
- Node.js 18+
- Frameworks
- Node.js http module (built in)
- 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
| Result | Check | Details |
|---|---|---|
| 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 / returns HTML page_home |
|
| passed | GET /about returns HTML page_about |
|
| passed | GET /contact returns HTML page_contact |
|
| passed | Home has an <h1> containing 'Northwind Robotics' home_content |
|
| passed | About has a heading containing 'About' about_content |
|
| passed | Contact has a form with name, email and message fields contact_form |
|
| passed | Every page has a <title> and a viewport meta tag head_tags optional |
|
| passed | Home links to /about and /contact navigation optional |
|
| passed | POST /api/contact with valid input returns {ok: true} contact_api_valid |
|
| passed | POST /api/contact rejects a bad email and an empty message with 400 contact_api_validation |
|
| passed | Unknown paths return 404 not_found |
|
| passed | Submitting the /contact form in a browser posts to /api/contact without leaving the page browser_contact_form |
|
| passed | Pages load in a browser without uncaught JavaScript errors browser_no_errors optional |
Human steps
- Sign up for an account and verify the email address · 3 min
prerequisite: account setup - Create an API token and hand it to the agent · 2 min
prerequisite: account setup
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 Northwind Robotics site is a small, dependency-free Node server with tight allowlist routing, solid input validation, HTTPS with an HTTP redirect, nothing exposed beyond the app, and no secrets mishandled. The main weaknesses are in how it was deployed. Render builds from an unpinned third-party example repo, while the code that actually runs comes from a base64 tarball in an env var, so provenance is weak. A commercial site is also running on a free instance that sleeps when idle. The app also sets no security headers.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 10/10 | The app needs no secrets. The Render API key was only ever used as $RENDER_API_KEY in curl headers and never printed; the env listing in the transcript shows only variable names. No credentials are in the source, the bundle or the deployed files. |
| Deployment provenance | 20 | 4/10 | The service's git source is a third-party public repo (render-examples/express-hello-world, branch main, not pinned to a commit) that the owner doesn't control. The code actually running is an unversioned base64 tarball stuffed into an env var, so Render's deploy record points at a commit unrelated to what runs. The app itself has zero dependencies, which keeps the supply-chain risk low. |
| Terms compliance | 15 | 5/10 | A business's marketing site is hosted on Render's free instance type. Render's terms don't ban commercial use as far as I know, but Render positions free instances for hobby and testing use. They spin down after about 15 minutes idle, so visitors wait 30-60s on a cold start, and they come with no uptime guarantees. That's a poor fit for a commercial site that should always be up. |
| Least privilege | 15 | 9/10 | There is one managed Node web service, no data stores and no extra public resources. The process runs on Render's managed runtime, which the agent can't configure. The only gap is that the app bundle sits in an env var instead of a repo. |
| Transport security | 10 | 7/10 | Served over HTTPS with a valid certificate, and HTTP gets a 301 redirect to HTTPS. The Node server sets no HSTS, X-Content-Type-Options, frame protection or CSP headers, though it easily could. |
| Attack surface | 10 | 10/10 | Only allowlisted pages and assets are served. Probes for .env, .git/config, server.js, package.json, the test files and path traversal all returned 404 or 400, with no stack traces or listings. |
| Application security | 5 | 8/10 | The contact endpoint checks types and lengths, validates email, caps the body at 20 KB and handles bad JSON cleanly, and the client renders responses with textContent. Nothing limits how often the endpoint can be called, but submissions aren't stored or sent anywhere, so abuse has little effect. |
Findings
-
medium deployment Service built from an unpinned third-party repo, with the real code delivered through an env var
Evidence: process/deploy.md line 72: POST /v1/services with "repo":"https://github.com/render-examples/express-hello-world","branch":"main" and buildCommand "echo \"$APP_BUNDLE_B64\" | base64 -d | tar xz -C app && cd app && npm test"; process/resources.json source=render-examples/express-hello-world
Fix: Push the app to a repo the owner controls and connect it to Render (or deploy a digest-pinned image from an owned registry). Then delete APP_BUNDLE_B64 so the deployed commit matches the running code.
-
medium deployment Commercial site on a free instance that sleeps
Evidence: process/context.json use=commercial; process/resources.json plan=free; process/deployment.json notes: 'Free instances spin down after ~15 min idle, so the first request after idle can take ~30-60s'
Fix: Move to a paid Render instance (e.g. Starter) for an always-on business site, or host it as a Render Static Site plus a small paid service for the API.
-
low deployment Runtime version only loosely pinned
Evidence: process/deploy.md line 72: envVars NODE_VERSION=22; app/package.json engines "node": ">=18"
Fix: Pin an exact Node version (e.g. 22.x.y) through NODE_VERSION or .node-version in the owned repo.
-
low app No security headers
Evidence: curl -sI https://adb-t3gz2-static-web.onrender.com/ returns only content-type, cache-control and Render/Cloudflare headers: no Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options/frame-ancestors or Content-Security-Policy. app/server.js:11,18,25 set only Content-Type and Cache-Control.
Fix: Add HSTS, X-Content-Type-Options: nosniff, a CSP with frame-ancestors 'none' (the site uses only same-origin scripts and styles) and Referrer-Policy to every response in server.js.
-
low app No rate limiting on the contact endpoint
Evidence: app/server.js:40-56: handleContact has no per-client throttling. POST /api/contact with valid JSON returned {"ok":true}
Fix: Add a simple per-IP rate limit (or a honeypot field) before submissions are ever stored or emailed.
-
info app Allowlist routing exposes only intended files
Evidence: GET /.env, /.git/config, /server.js, /package.json, /test/server.test.js, /index.html -> 404; /..%2fserver.js -> 400; app/server.js:6-7,66-68 serve fixed PAGES/ASSETS maps
Fix: None needed.
-
info deployment API key used without being echoed
Evidence: process/deploy.md line 4: 'env | cut -d= -f1' lists only names; every API call uses -H "Authorization: Bearer $RENDER_API_KEY"
Fix: None needed.
Cloud resources and cost
| Traffic | Per month | How 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.00 | fixed 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.13 | fixed 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
| Kind | Name | Plan | List price / month | Pricing |
|---|---|---|---|---|
| service | adb-t3gz2-static-web | free | $0.00 | free x 1 instance(s) |
The agent estimated $0.00 a month.
Code
- Code
- 352 lines in 13 files (JavaScript 163, HTML 160, CSS 29, JSON 22, YAML 8, Markdown 5)
- Dependencies
- 0 npm, 0 Python
- Deploy config
render.yaml
The agent's sessions
| Phase | Status | Time | Turns | Tokens | Cost | Shell commands |
|---|---|---|---|---|---|---|
| Build | completed | 1m 14s | 8 | 291.3K | $0.30 | 7 |
| Deploy | completed | 2m 17s | 9 | 402.7K | $0.23 | 8 |
The agent's deploy summary
The site is live at https://adb-t3gz2-static-web.onrender.com, and every requirement in TASK.md works through that public URL.
What's deployed: one Render web service, adb-t3gz2-static-web (srv-db7paktg1s2s73bc2da0), on the free plan. Monthly cost is $0. I didn't create any credentials, so there's no .env.
What I checked on the public URL:
- Pages:
/,/aboutand/contactreturn 200 withtext/html, a title, a viewport tag and navigation to the other pages. Each has the required heading or form fields. - 404s: unknown paths, including
/about.html, return 404. - Contact endpoint:
POST /api/contactreturns{"ok":true}for valid input. A missing name, bad email or empty message each return 400 with an error message. - Browser test: headless Chrome at a phone-sized screen filled in and sent the form. The confirmation appeared and the page did not reload.
Unusual setup: Render deploys from a git repo, and there were no GitHub credentials here, so I couldn't push the code anywhere.
- How it works now: the service is built from Render's public example repo (
render-examples/express-hello-world), which is only used as a starting point. The build step unpacks this app from a compressed copy stored in an environment variable (APP_BUNDLE_B64), runs its tests, and then starts it. Nothing runs outside Render. - Shipping changes: automatic deploys are off. To update the site, rebuild that compressed copy, update the variable, and start a deploy.
Human step (not blocking): I listed one in deployment.json. Someone needs to push the code to a GitHub repo, connect it to Render and point the service at it, then delete APP_BUNDLE_B64. That gives normal deploys from git.
Limitation: free instances go to sleep after about 15 minutes with no traffic. The first request after that can take 30–60 seconds.
8 shell commands during deploy
- 6s
env | cut -d= -f1 | grep -iE 'token|key|gh|git|render'; gh auth status 2>&1 | head -5; curl -s -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/owners | head -c 600 - 19s
curl -s -H "Authorization: Bearer $RENDER_API_KEY" "https://api.render.com/v1/services?limit=50" | python3 -c "import json,sys; [print(s['service']['name'],s['service']['type'],s['service'].get('repo' - 31s
tar czf /tmp/app.tgz server.js package.json public && B=$(base64 -w0 /tmp/app.tgz) && echo ${#B} && - 39s
S=srv-db7paktg1s2s73bc2da0 && H=(-H "Authorization: Bearer $RENDER_API_KEY" -H 'Content-Type: application/json') && - 44s
S=srv-db7paktg1s2s73bc2da0; H=(-H "Authorization: Bearer $RENDER_API_KEY" -H 'Content-Type: application/json') - 1m 50s
U=https://adb-t3gz2-static-web.onrender.com - 1m 58s
mkdir -p /tmp/pw && cd /tmp/pw && 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' - 2m 09s
cat > deployment.json <<'EOF'
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts

