Render · Opus 5.5 · medium effort · HIPAA organization · trial 1
Data / Visualization App on Render
Success
Deployed to https://adb-zvm8n-dataviz-web.onrender.com. Every deployment is deleted after it's verified and reviewed.
Screenshots
Technologies
- Runtime
- Node.js 22
- Frameworks
- Node.js http module (built in)
- 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
| 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 / serves the dashboard as HTML dashboard |
|
| passed | GET /api/summary (no filters) matches the dataset summary_all |
|
| passed | GET /api/summary (region=West) matches the dataset summary_region |
|
| passed | GET /api/summary (category=Electronics) matches the dataset summary_category |
|
| passed | GET /api/summary (region=East, category=Furniture, start=2025-03-01, end=2025-06-30) matches the dataset summary_combined |
|
| passed | GET /api/summary (region=Atlantis) matches the dataset summary_empty |
|
| passed | The dashboard renders a chart (SVG or canvas) in a browser browser_chart |
|
| passed | The dashboard shows at least two charts browser_two_charts optional |
|
| passed | The dashboard has filter controls browser_filters 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
Stack fit
The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.
A zero-dependency Node server with in-memory CSV aggregation and vanilla SVG charts fits this small read-only dashboard well and matches the reasoning in STACK.md. The deployment departed from the planned Git-backed native Node service and runs as an image-backed workaround on a sleeping free instance.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Fit for the job | 40 | 9/10 | Keeping the read-only 2,400-row CSV in memory is exact and fast, integer-cent sums give correct rounding, and plain fetch plus inline SVG covers the filter, KPI and chart needs without page reloads. |
| Proportion | 30 | 9/10 | One process, the Node standard library only, no database and no build step: about as simple as the requirements allow. The only extra complexity is the secret-file boot shim forced by the delivery method. |
| Platform fit | 30 | 6/10 | A Node web service suits Render, and STACK.md and render.yaml plan for the native runtime. What was deployed instead is an image-backed service that unpacks code from secret files, which works around Render's build pipeline, and it runs on a free instance that sleeps. |
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).
A small, zero-dependency Node dashboard with careful input validation, an allowlist for static files, and no secrets to leak. Its weaknesses are in the deployment: a commercial app runs on Render's sleeping free tier, the code is shipped as a tarball in a Render secret file on a floating node:22-alpine tag rather than built from an owned repo, and the container runs as root with no security headers.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 10/10 | The app needs no secrets. The Render API key was only read from the environment, and the transcripts never print it (the deploy check prints just 'key-set'). .env is git-ignored and no credentials are in the source or the shipped bundle. |
| Deployment provenance | 20 | 6/10 | What runs is the committed git HEAD, and it has no third-party dependencies. But it is shipped as a base64 tarball in a Render 'secret file' and unpacked at boot, not built from a repo the owner controls, so there is no traceable source-to-build link. The base image is the floating tag node:22-alpine, not pinned to a digest. |
| Terms compliance | 15 | 4/10 | This is a business (commercial) app on Render's free instance type. Render says free instances are for hobby and testing work, not production. They spin down after 15 minutes idle with 30-60 s cold starts, and draw on a capped pool of monthly free hours. The agent noted the sleep but didn't choose a paid Starter plan. |
| Least privilege | 15 | 7/10 | There is one stateless web service with no data store and no extra public resources. The container runs as the node:22-alpine default user (root), because the custom dockerCommand doesn't drop to the image's 'node' user. |
| Transport security | 10 | 8/10 | HTTP returns 301 to HTTPS and the certificate is valid. The app sends none of HSTS, X-Content-Type-Options, X-Frame-Options/frame-ancestors or CSP, though its own server could easily have set them. |
| Attack surface | 10 | 9/10 | Only an explicit allowlist of three static files is served. .env, .git, source, the CSV and the secret bundle all return JSON 404s, and errors return no stack traces. The only extra is a harmless /healthz that reports the row count. |
| Application security | 5 | 9/10 | Dates are strictly validated (400 on bad input), filters are exact-match comparisons in memory with no injection surface, page size is clamped to 1-500 and offset to at least 0, and the client renders all data with textContent. There is no app-level rate limiting, but endpoints are cheap and read-only. |
Findings
-
medium deployment Commercial app on Render free instance that sleeps
Evidence: process/resources.json: plan "free"; process/context.json: use "commercial"; process/deployment.json notes: "The free plan spins the service down after about 15 minutes idle, so the first request after that takes about 30-60 seconds."
Fix: Move the service to a paid instance type (e.g. Starter) so it stays up and runs under production-grade terms.
-
medium deployment App code delivered as a secret-file tarball instead of a source-controlled build
Evidence: app/scripts/deploy.sh: git archive -> base64 -> secretFiles 'app.tgz.b64'; app/scripts/render-boot.js unpacks /etc/secrets/app.tgz.b64 into /tmp/app and requires server.js
Fix: Push the repo to a Git provider the owner controls and deploy with the included render.yaml (native Node runtime), or build and push a versioned image to an owned registry.
-
low deployment Base image not pinned to a digest
Evidence: app/scripts/deploy.sh: "imagePath": "docker.io/library/node:22-alpine" (floating tag; Render pulls whatever the tag points to on each deploy)
Fix: Pin the image as node:22-alpine@sha256:<digest> and update it on purpose.
-
low deployment Container process runs as root
Evidence: app/scripts/deploy.sh: dockerCommand 'node /etc/secrets/boot.js' on node:22-alpine, whose default user is root; nothing switches to the 'node' user
Fix: Run as the image's unprivileged 'node' user (custom image with USER node), or use Render's native Node runtime.
-
low app No security headers
Evidence: GET https://adb-zvm8n-dataviz-web.onrender.com/ response headers: content-type, rndr-id, server: cloudflare, vary, x-render-origin-server, cf-cache-status, cf-ray, alt-svc; no Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options or Content-Security-Policy. app/server.js writeHead sets only Content-Type/Cache-Control.
Fix: In server.js, add HSTS, X-Content-Type-Options: nosniff, X-Frame-Options: DENY (or CSP frame-ancestors 'none'), and a strict CSP (default-src 'self').
-
info app Health endpoint is public
Evidence: GET /healthz -> 200 {"status":"ok","rows":2400}
Fix: Acceptable. Optionally return only {status:'ok'}.
-
info app No rate limiting on public API
Evidence: app/server.js: /api/summary and /api/orders are served without any throttling; GET /api/orders?limit=100000&offset=-5 -> 200 with limit clamped to 500, offset 0
Fix: Low risk for in-memory aggregation of 2,400 rows. Add simple per-IP limits if traffic grows.
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-zvm8n-dataviz-web | free | $0.00 | free x 1 instance(s) |
The agent estimated $0.00 a month.
Code
- Code
- 636 lines in 15 files (JavaScript 434, CSS 82, HTML 66, Shell 54, Markdown 26, JSON 21, YAML 13)
- Dependencies
- 0 npm, 0 Python
- Deploy config
render.yaml
The agent's sessions
| Phase | Status | Time | Turns | Tokens | Cost | Shell commands |
|---|---|---|---|---|---|---|
| Build | completed | 2m 24s | 18 | 607.8K | $0.59 | 8 |
| Deploy | completed | 2m 30s | 11 | 755.8K | $0.38 | 10 |
The agent's deploy summary
The sales dashboard is live at https://adb-zvm8n-dataviz-web.onrender.com, and every TASK.md requirement passes when tested through that public HTTPS address.
What's deployed: one web service, adb-zvm8n-dataviz-web (id srv-db81e8mq1p3s73972p1g), on Render's free plan in Oregon. It costs $0 a month and needs no database.
How it got there: Render normally builds from a Git repository, but there were no GitHub credentials and no repo connected to the Render team. So instead:
- The service runs the official public Node image,
node:22-alpine, which Render pulls itself. I didn't push anything to any registry. - The committed app (about 47 KB) is attached to the service as a Render "secret file". The app has no secrets of its own.
- A small startup script,
scripts/render-boot.js, unpacks the app and starts the server. - To redeploy the current commit, run
scripts/deploy.sh. It updates the files on Render, starts a new deploy, waits until it's live and runs the checks. I ran it once and it worked. - If the repo is pushed to GitHub later,
render.yamlsets up the normal Git-based deploy instead.
Tests on the public URL:
- Smoke tests: all 6 checks in
scripts/smoke.shpass. They cover the dashboard page, totals with and without filters (matching the CSV), the empty result, and the orders table. - Bad dates: an invalid date gets a
400response. - Browser: in a headless browser, changing filters updated the figures and table without reloading the page, with no page errors.
Limitation: on the free plan the service goes to sleep after about 15 minutes with no traffic. The first request after that takes 30–60 seconds.
Nothing needed a person, so human_steps in deployment.json is empty. I didn't create any new credentials, and .env is excluded from git.
10 shell commands during deploy
- 9s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; test -n "$RENDER_API_KEY" && echo key-set; gh auth status 2>&1 | head -2; env | grep -iE 'github|gitlab|token' | sed 's/=.*/=<set>/' - 28s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; mkdir -p /tmp/bundle; git archive --format=tar.gz -o /tmp/bundle/app.tgz HEAD; ls -l /tmp/bundle/app.tgz; base64 -w0 /tmp/bundle/app.tgz | wc -c; tar tzf - 37s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; cat > scripts/render-boot.js <<'EOF' - 50s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; timeout 5 docker run --rm -e PORT=10000 -v /tmp/bundle:/etc/secrets:ro node:22-alpine node /etc/secrets/boot.js 2>&1 | tail -8 - 58s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; python3 - <<'EOF' - 1m 19s
cd /tmp/bundle; python3 - <<'EOF' > /tmp/bundle/create.json - 1m 25s
for i in $(seq 1 40); do st=$(curl -sS -H "Authorization: Bearer $RENDER_API_KEY" -H "Accept: application/json" "https://api.render.com/v1/services/srv-db81e8mq1p3s73972p1g/deploys?limit=1" | python3 - 1m 40s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; U=https://adb-zvm8n-dataviz-web.onrender.com; scripts/smoke.sh $U; curl -sS -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' $U/api/summary?start=bad - 2m 02s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; cat > scripts/deploy.sh <<'EOF' - 2m 06s
cd /home/runner/work/_temp/adb-zvm8n-dataviz; scripts/deploy.sh 2>&1 | uniq | tail -12
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts
