Railway · Opus 5.5 · medium effort · HIPAA organization · trial 1
Static Content Site on Railway
Success
Deployed to https://adb-yz0s2-static-web-production.up.railway.app. Every deployment is deleted after it's verified and reviewed.
Screenshots
Technologies
- Runtime
- Node.js 18+
- Frameworks
- Node.js http module (built in)
- Packaging
- railway.json
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 - Add a payment method to the account · 2 min
resources: paid resources: adb-yz0s2-static
Stack fit
The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.
A dependency-free Node.js server on a single Railway service fits a three-page site with one same-origin form endpoint well. STACK.md explains the choice soundly, and the build matches it.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Fit for the job | 40 | 9/10 | Node's built-in http module serves exact extensionless routes, real 404s and a validated JSON endpoint on the same origin, which covers everything TASK.md asks for, and the app needs no data store. |
| Proportion | 30 | 10/10 | It is one roughly 100-line file with zero dependencies and no build step, using the built-in node:test runner, which is about as small as these requirements allow. |
| Platform fit | 30 | 8/10 | Railway has no static-only hosting or functions, so one small always-on service serving both pages and the API is the natural fit there, and it costs about $1/month. Static files are served from a container, which is acceptable on this platform. |
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 deployed cleanly to Railway with no secrets involved. HTTPS is enforced with a valid certificate and only allowlisted paths are exposed. The remaining weaknesses are minor: no security headers, an unpinned Node runtime and Railpack base image, a local `railway up` upload with no source repository, a likely root container, and no rate limiting on the contact form.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 10/10 | The app needs no secrets. The Railway token came only from the environment, was never echoed (the transcript shows only 'token set'), and .env is excluded by .gitignore and .railwayignore. No credentials show up in source, logs or deployed files. |
| Deployment provenance | 20 | 7/10 | Local source was uploaded with `railway up`, built by Railpack and checked live, and there are no npm dependencies. Two inputs are not pinned: the Node version (engines '>=18', so Railpack picked Node 24) and the Railpack base image chosen by the platform. There is also no source repository behind the deployed artifact. |
| Terms compliance | 15 | 7/10 | This is a commercial app on what the agent describes as the account's existing usage-based Railway plan. Railway's paid plans allow commercial use and do not sleep services, but context.json does not name the plan, so I can't confirm the account isn't on the trial/free tier, which has credit limits. Separately, railway.json config-as-code is deprecated after 2026-12-01. |
| Least privilege | 15 | 7/10 | There is a single service with no data store, volume or extra public resources, and only the needed HTTP port is exposed. The Railpack image runs as its default (root) user, and the agent took no steps to drop privileges. |
| Transport security | 10 | 7/10 | HTTPS works with a valid Let's Encrypt certificate, and HTTP returns a 301 to HTTPS. The app sets none of HSTS, X-Content-Type-Options, frame protection or CSP, even though it fully controls its response headers. |
| Attack surface | 10 | 9/10 | Routing uses an exact-path allowlist, so .env, .git, server.js, package.json, *.html and traversal attempts all return 404, with no listings or stack traces. The only extra endpoint is a harmless /healthz. |
| Application security | 5 | 8/10 | The contact endpoint checks types, required fields and email format, limits bodies to 64 KB (returns 413), handles bad JSON cleanly, and the client renders responses with textContent, so there is no XSS. There is no rate limiting or bot protection on the public form, and submitter emails are written to the logs. |
Findings
-
low app No security headers set
Evidence: curl -sI https://adb-yz0s2-static-web-production.up.railway.app/ returns only content-type, date, server, x-railway-* and content-length; there is no Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options/frame-ancestors or Content-Security-Policy. server.js:83-98 writes only Content-Type and Content-Length.
Fix: Add HSTS, X-Content-Type-Options: nosniff, X-Frame-Options: DENY (or CSP frame-ancestors 'none') and a strict CSP such as default-src 'self' to every response in server.js.
-
low deployment Node runtime and build base image not pinned
Evidence: app/package.json engines {"node": ">=18"}; deployment.json reports Node.js 24 picked by Railpack; railway.json uses builder RAILPACK with no pinned image or version.
Fix: Pin an exact Node major/minor (engines or .node-version / RAILPACK_NODE_VERSION), or deploy from a Dockerfile with a digest-pinned base image.
-
low deployment Deployed from an untracked local upload
Evidence: deploy.md: `railway up --service adb-yz0s2-static-web --ci`; deployment.json notes 'no GitHub repo, no container registry'.
Fix: Deploy from a version-controlled repository the owner controls (for example Railway's GitHub integration), so every deployment maps to a commit.
-
low deployment Container likely runs as root
Evidence: railway.json uses the default Railpack build with no user configuration; Railpack images run as root by default, and the build log shows apt/libc-bin steps in the image.
Fix: Use a Dockerfile with a non-root USER (for example node), or configure the build to drop root privileges.
-
low app Contact endpoint has no rate limiting
Evidence: server.js:46-70 accepts every valid POST /api/contact; a POST of {"name":"<script>","email":"a@b.co","message":"hi"} returned 200 {"ok":true}, and no throttling exists.
Fix: Add a simple per-IP rate limit and/or a honeypot field before submissions are ever stored or emailed.
-
info app Submitter email addresses logged
Evidence: server.js:67 console.log(`Contact form submission from ${data.email.trim()}`)
Fix: Avoid logging personal data, or mask it and document log retention.
-
info deployment Plan not verifiable; config format deprecated
Evidence: process/context.json has an empty account object; deployment.json says "account's existing Railway plan (usage-based)" and notes that railway.json is deprecated after 2026-12-01.
Fix: Confirm the account is on a paid Hobby or Pro plan (not the trial) for commercial uptime, and run `railway config migrate` before 2026-12-01.
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. | $5.28 | memory billed all month, CPU for ~10 h of activity, 0.04 GB egress at $0.05/GB |
| 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. | $27.63 | memory billed all month, CPU for ~730 h of activity, 52.6 GB egress at $0.05/GB |
Resources the agent created
| Kind | Name | Plan | List price / month | Pricing |
|---|---|---|---|---|
| project | adb-yz0s2-static | 1 service(s) | $25.00 | 1 service(s) at the assumed 1 vCPU / 0.5 GB each, always on; usage is metered per second so a mostly idle service costs less than this list price |
The agent estimated $1.00 a month.
Code
- Code
- 418 lines in 16 files (JavaScript 208, HTML 166, JSON 36, CSS 33, Markdown 26, Shell 11)
- Dependencies
- 0 npm, 0 Python
- Deploy config
railway.json
The agent's sessions
| Phase | Status | Time | Turns | Tokens | Cost | Shell commands |
|---|---|---|---|---|---|---|
| Build | completed | 2m 00s | 13 | 500.4K | $0.52 | 12 |
| Deploy | completed | 2m 17s | 7 | 353.2K | $0.15 | 6 |
The agent's deploy summary
The site is live at https://adb-yz0s2-static-web-production.up.railway.app, and every requirement in TASK.md works through that address.
What I deployed
- A Railway project
adb-yz0s2-staticwith one web service,adb-yz0s2-static-web, in the production environment. - I uploaded the code straight from this directory with
railway up, so no GitHub repo or image registry was involved. The service has a Railway-provided HTTPS address and a health check on/healthz. - Estimated cost is about $1/month, which the plan's included usage should cover.
What I tested through the public address
- Pages:
/,/aboutand/contactreturn 200 as HTML, each with the required heading or form fields. - 404s: unknown paths and
.htmlsuffixes (e.g./about.html) return 404. - Form endpoint:
POST /api/contactreturns 200{"ok":true}for valid input. A missing name, invalid email or empty message each return 400 with a reason. - HTTPS: plain HTTP redirects to HTTPS.
- Browser: in headless Chromium at phone width, the form showed its confirmation without reloading the page, and an invalid email showed the error message.
Things to know
- Config format: Railway now flags
railway.jsonas deprecated. It keeps working until 2026-12-01; runningrailway config migratebefore then moves it to the new format. - Credentials: I used only the token you provided and created none, so there is no
.envandhuman_stepsindeployment.jsonis empty.
deploy.sh and the .railwayignore from earlier are still in the directory. I didn't run the script, since I did the same steps by hand.
6 shell commands during deploy
- 4s
test -n "$RAILWAY_API_TOKEN" && echo token set; which railway || npm i -g @railway/cli >/dev/null 2>&1; railway whoami 2>&1 | head -3; railway list 2>&1 | head - 9s
railway init --name adb-yz0s2-static --json 2>&1 | tail -5; railway add --service adb-yz0s2-static-web --json 2>&1 | tail -5 - 13s
railway up --service adb-yz0s2-static-web --ci 2>&1 | tail -15 - 1m 46s
railway domain --service adb-yz0s2-static-web --json 2>&1 | tail -3; railway status --json 2>/dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);e=d['environments']['edges'][0]['node'];prin - 1m 54s
U=https://adb-yz0s2-static-web-production.up.railway.app; for i in $(seq 1 30); do [ "$(curl -s -o /dev/null -w '%{http_code}' $U/)" = 200 ] && break; sleep 5; done - 2m 09s
cat > deployment.json <<'EOF'
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts

