Railway · Opus 5.5 · medium effort · HIPAA organization · trial 1
Static Content Site on Railway
Success
Deployed to https://adb-ytghj-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
Stack fit
The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.
The agent chose a zero-dependency Node HTTP server as one Railway service, a minimal and well-reasoned fit for three static pages plus one validation endpoint, and the build matches the reasoning in STACK.md.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Fit for the job | 40 | 9/10 | Plain node:http with an in-memory allowlist meets the requirements exactly: extensionless paths, real 404s and a same-origin JSON endpoint. No storage is needed, and none is used. |
| Proportion | 30 | 10/10 | The whole app is one ~80-line file with zero dependencies, one service and no build step, which is as small as these requirements allow. STACK.md weighs Express and Caddy plus a function, and rejects them for sound reasons. |
| Platform fit | 30 | 8/10 | Railway is a container platform, and a single always-on service fits it: it deploys with zero configuration through Railpack, uses $PORT and a health check, and costs about $1/month. Serving mostly static content from an always-on process is a small mismatch, but it is justified by the same-origin API. |
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, dependency-free Node server on Railway serves the three pages and a well-validated contact endpoint over HTTPS, with no secrets involved and nothing exposed beyond the app. The weaknesses are minor: no HSTS, CSP or frame headers, no rate limiting, raw personal data in the logs, a likely root container, a local upload with no committed revision and an unpinned Node version, and an account plan nobody checked for commercial use.
| Criterion | Weight | Score | Why |
|---|---|---|---|
| Secrets handling | 25 | 10/10 | The app needs no secrets. The Railway token stayed in the session environment, deploy.sh reads it from env/.env, and .env is excluded by both .gitignore and .railwayignore. No secret appears in the source, the transcripts or the deployed files. |
| Deployment provenance | 20 | 7/10 | Railway built the image with Railpack from the agent's local upload (`railway up`), and that build is what runs. But no commit or repo ties the upload to a reviewed revision, and the Node version is left floating (`engines: >=18`, which resolved to Node 24) while the Railpack base images are platform defaults that aren't pinned. |
| Terms compliance | 15 | 7/10 | Railway's paid usage-based plans allow commercial use, and an always-on service fits a marketing site. But process/context.json does not show the account plan, and the agent never checked it. If the account is on the Trial/free tier, its credit limits could stop the service, so I can't fully confirm this fits a commercial app. |
| Least privilege | 15 | 8/10 | There is one public web service, with no data stores, extra services or extra variables. The Railpack image almost certainly runs the Node process as root, because the agent didn't configure a non-root user. |
| Transport security | 10 | 7/10 | The site is HTTPS-only with a valid Let's Encrypt certificate, and HTTP redirects to HTTPS with a 301. The app sets X-Content-Type-Options, but not HSTS, CSP or frame protection, even though it controls every response header. |
| Attack surface | 10 | 10/10 | The server serves only an allowlist of files held in memory. /.env, /.git/config, /server.js, /package.json, /railway.json, path traversal and *.html paths all return the custom 404. There is no directory listing and no stack trace, and the only extra endpoint is a trivial /health. |
| Application security | 5 | 8/10 | Input validation is strict: types are checked, lengths are capped, request bodies are limited to 64 KB, bad JSON gets a 400, and the client writes messages with textContent, so there is no XSS. There is no rate limiting on the public form endpoint, and the server logs submitters' raw names and emails, which allows log injection and keeps personal data in the logs. |
Findings
-
low app Missing HSTS, CSP and frame-protection headers
Evidence: curl -sI https://adb-ytghj-static-web-production.up.railway.app/ returns only content-type, x-content-type-options: nosniff and the Railway edge headers. server.js:68 sets only Content-Type, Content-Length and X-Content-Type-Options.
Fix: Add Strict-Transport-Security, Content-Security-Policy (default-src 'self'), and X-Frame-Options: DENY or frame-ancestors 'none' in server.js.
-
low app No rate limiting on POST /api/contact
Evidence: server.js:31-49 accepts unlimited submissions from any client, with no throttle or bot check.
Fix: Add a simple per-IP rate limit (for example, an in-memory token bucket keyed on X-Forwarded-For) or a honeypot field.
-
low app Unsanitised personal data written to logs (log injection)
Evidence: server.js:46 logs `contact: ${name} <${email}>`. POST /api/contact with {"name":"x\nINJECT fake log",...} returned 200 {"ok":true}, so the newline goes into the log line.
Fix: Escape control characters (or log JSON.stringify'd values), and consider not logging submitters' names and emails at all.
-
low deployment Local upload with an unpinned runtime and no committed revision
Evidence: deploy.md: `railway up --service adb-ytghj-static-web --ci` was run from a working tree whose files were staged but never committed. package.json:11 has "engines": {"node": ">=18"}, and Railpack resolved that to Node 24 on its default base image.
Fix: Commit the code and deploy from a connected repo or a tagged commit, and pin the Node version (for example, "node": "24.x" or a .node-version file).
-
low deployment Container likely runs as root
Evidence: railway.json uses builder RAILPACK with no user configuration, and Railpack images run the start command as root by default.
Fix: Use a Dockerfile with a non-root USER (for example, `USER node`), or configure Railpack to drop privileges.
-
info deployment Account plan not verified for commercial use
Evidence: process/context.json has "use": "commercial" and "account": {}. deployment.json says only "Existing account plan (unchanged)".
Fix: Confirm the workspace is on a paid Railway plan (Hobby or Pro) rather than the Trial/free tier, so the commercial site isn't stopped when credits run out.
-
info platform Config-as-code file deprecated
Evidence: The Railway CLI warned that railway.json is deprecated and keeps working only until 2026-12-01 (deploy.md).
Fix: Run `railway config migrate` before 2026-12-01 so the health check and restart policy keep applying.
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 | nothing deployed |
| 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. | $0.00 | nothing deployed |
The benchmark found no cloud resources for this cell.
The agent estimated $1.00 a month.
Code
- Code
- 372 lines in 16 files (JavaScript 168, HTML 161, JSON 34, CSS 32, Markdown 24, 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 | 1m 49s | 10 | 380.9K | $0.36 | 9 |
| Deploy | completed | 1m 36s | 7 | 340.4K | $0.15 | 6 |
The agent's deploy summary
The site is live at https://adb-ytghj-static-web-production.up.railway.app and every requirement in TASK.md works through that URL.
What's on Railway:
- Project:
adb-ytghj-static(b35149d1-38b2-4bed-8220-7bb444c4738c), with itsproductionenvironment. - Service:
adb-ytghj-static-web(2825be50-3d2b-41bc-8920-1e0eb705e58c), one copy of the Node server, which serves the pages and/api/contactfrom the same address. - Address: the free HTTPS address Railway generated for the service.
I uploaded the code straight from this directory with railway up; no GitHub repo is connected.
Tested through the public URL:
/,/aboutand/contactreturn 200 astext/html, each with a title, the viewport tag, navigation and the right<h1>./about.htmland an unknown path return 404.POST /api/contactreturns 200{"ok":true}for valid input, and 400 with an error for a missing name, an invalid email or an empty message.- In headless Chrome at phone width, the contact form showed its confirmation without reloading the page, and nothing overflowed sideways.
Cost: about $1 a month or less of Railway's usage-based billing for one small always-running service. I estimated that rather than reading it from Railway. The account plan was not changed, and the plan's included credit should cover it.
Nothing needed a person, so human_steps is empty, and I created no new credentials, so there is no .env. The Railway CLI warns that railway.json is deprecated; it keeps working until 2026-12-01. deployment.json has the IDs and notes. The files are still staged in git but not committed.
6 shell commands during deploy
- 4s
cd /home/runner/work/_temp/adb-ytghj-static; railway --version; railway whoami 2>&1; railway list 2>&1 | head - 8s
cd /home/runner/work/_temp/adb-ytghj-static; railway init --name adb-ytghj-static 2>&1 | tail -5; railway add --service adb-ytghj-static-web 2>&1 | tail -5 - 19s
cd /home/runner/work/_temp/adb-ytghj-static; railway up --service adb-ytghj-static-web --ci 2>&1 | grep -v -i "config as code\|migrate\|keep working" | tail -15 - 1m 01s
cd /home/runner/work/_temp/adb-ytghj-static; railway domain --service adb-ytghj-static-web 2>&1 | grep -i http; railway status --json 2>/dev/null | python3 -c " - 1m 08s
U=https://adb-ytghj-static-web-production.up.railway.app; for i in $(seq 1 20); do [ "$(curl -s -o /dev/null -w '%{http_code}' $U/)" = 200 ] && break; sleep 5; done - 1m 25s
cd /home/runner/work/_temp/adb-ytghj-static; cat > deployment.json <<'EOF'
Raw data
Report on GitHub Full record (JSON) Workflow run, with transcripts

