Railway · Opus 5.5 · medium effort · HIPAA organization · trial 1

Database-Backed CRUD App on Railway

Success

Checks passed19/19
Human steps37 min of a person's time
Deploy without a human Failed
Deploy with a human Deployed
Security & compliance80/100
Stack fit90/100
Build2m 49s
Deploy2m 57s
Tokens2.24M
Agent cost$1.19
Hosting / month$10.55 light use$52.63 busy

Deployed to https://adb-sufaa-crud-web-production.up.railway.app. 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-sufaa-crud-web-production.up.railway.app/
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-sufaa-crud-web-production.up.railway.app/

Technologies

Runtime
Node.js 20+
Frameworks
Express
Libraries
node-postgresbcrypt.js

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

Stack fit

The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.

Node/Express 5 with Railway's managed Postgres is a simple, well-sized stack for a multi-user CRUD app, and it uses Railway's strengths (managed DB, private networking, Railpack builds). STACK.md justified the choice soundly.

CriterionWeightScoreWhy
Fit for the job 40 9/10 Railway's managed Postgres gives durable, relational storage for users and todos, and bcryptjs, pg and Express 5 (which handles async errors) cover the requirements directly. A vanilla-JS page on the same API is enough for this UI.
Proportion 30 9/10 There are two services, three runtime dependencies and one server file. The schema is created on startup, with no ORM, frontend framework or extra services, and nothing that only appears to work.
Platform fit 30 9/10 A stateless Node container built by Railpack, with a health check, restart policy, private networking and a reference variable for the DB, is the idiomatic Railway setup. STACK.md reasons about this sensibly, and the build matched it apart from moving from Express 4 to Express 5.

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 has clean secrets handling: Postgres is private and reached through a Railway reference variable, queries are parameterized, passwords use bcrypt, session tokens are stored hashed, and todos are correctly scoped to their owner. The live site is HTTPS-only and exposes nothing beyond the app. The weaknesses are minor: no rate limiting on login or signup, no security headers, an unpinned Node runtime and DB image deployed from a local directory, a superuser DB connection with a probably-root container, and a Railway plan tier that couldn't be checked against commercial use.

CriterionWeightScoreWhy
Secrets handling 25 10/10 No credentials appear in source, logs or deployed files. DATABASE_URL is a Railway reference variable (${{adb-sufaa-crud-db.DATABASE_URL}}) read from process.env, and when the agent checked the variable it printed only the hostname.
Deployment provenance 20 7/10 The service was deployed straight from the agent's own source with `railway up` and built by Railpack, and a package-lock.json pins the dependencies. But the Node runtime is unpinned (engines >=20, which resolved to Node 24), the Railpack builder image is chosen by the platform, and Postgres uses the floating template tag postgres-ssl:18. Deploying from a local directory also leaves no commit to trace the running build back to.
Terms compliance 15 7/10 The project sits on the account's existing usage-based Railway plan with an always-on service and a managed Postgres, and as far as I know Railway's paid plans allow commercial use. The exact plan tier couldn't be read (context.json has an empty account), so I can't rule out a trial or Hobby tier that is less suited to a business app.
Least privilege 15 7/10 Postgres is reached only over the private network (postgres.railway.internal) and has no public URL, and only the web service has a public domain. The app connects as the template's default superuser, and the Railpack container most likely runs as root.
Transport security 10 8/10 HTTPS works with a valid Let's Encrypt certificate, and HTTP gets a 301 redirect to HTTPS. Neither the app nor the platform sends HSTS, X-Content-Type-Options, frame protection or a CSP, and the app could easily have added them.
Attack surface 10 9/10 Only public/ is served statically, and source files, .env, .git and package files all return 404. Errors come back as generic JSON with no stack traces. The only extras are /healthz, which reveals nothing, and Express's default 'Cannot GET' page.
Application security 5 7/10 All queries are parameterized and every todo query is scoped by user_id. Passwords are hashed with bcrypt, tokens are random and stored only as SHA-256 hashes with a 30-day expiry and logout revocation, and titles are rendered with textContent. Login and signup have no rate limiting, titles have no length cap, and the token lives 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.
$10.55memory 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.
$52.63memory billed all month, CPU for ~730 h of activity, 52.6 GB egress at $0.05/GB

Resources the agent created

KindNamePlanList price / monthPricing
projectadb-sufaa-crud2 service(s)$50.002 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 $3.00 a month.

Code

Code
441 lines in 9 files (JavaScript 287, HTML 137, Markdown 41, JSON 31, Shell 17)
Dependencies
3 npm, 0 Python
Deploy config
None

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 49s12488.8K$0.4811
Deploycompleted2m 57s201.26M$0.4819

The agent's deploy summary

The app is live on Railway at https://adb-sufaa-crud-web-production.up.railway.app. Every requirement in TASK.md passed when tested through that public URL.

What's running (Railway project adb-sufaa-crud, production environment):

  • adb-sufaa-crud-web: the Node/Express app, built from this directory with railway up, with an HTTPS address on Railway's own domain. It has a health check at /healthz and restarts automatically if it crashes.
  • adb-sufaa-crud-db: Railway's managed Postgres 18 on a 500 MB volume. The web service connects to it over Railway's internal network using a variable reference, so no database password is stored in any file.

What I tested through the public URL:

  • Test suite: the full API test suite passed. I changed it so it can target a deployed URL (TEST_BASE_URL=<url> npm test). It covers signup and login, every error code (400, 401, 404, 409), create/rename/complete/delete, oldest-first ordering, and users not seeing each other's todos.
  • Web UI: a headless Chrome session signed up, added, completed, renamed and deleted todos, then logged out.
  • Persistence: I created a todo, redeployed the web service, logged in again, and the todo was still there.

Changes from the earlier plan:

  • Railway's CLI created the database service as "Postgres", so I renamed it to adb-sufaa-crud-db through Railway's API.
  • Railway warned that railway.json is deprecated and stops working on 2026-12-01. I deleted it and set the health check and restart policy directly on the service instead. deploy.sh now records the exact setup commands, but I haven't re-run it from scratch.

Cost: about $3 a month on the account's existing usage-based plan. That's my estimate for a small, mostly idle app plus the database, and it will grow with traffic.

Nothing needed a person, so human_steps in deployment.json is empty. I used the provided token and created no new credentials, so there is no .env. deployment.json has the resource IDs and notes.

19 shell commands during deploy
  1. 3s test -n "$RAILWAY_API_TOKEN" && echo token-set; railway whoami 2>&1; railway list --json 2>&1 | head -30
  2. 7s railway init --name adb-sufaa-crud --json 2>&1 | tail -5 && railway add --database postgres --service adb-sufaa-crud-db 2>&1 | tail -5; railway status --json 2>&1 | node -e 'const s=JSON.parse(require
  3. 16s railway service --help 2>&1 | sed -n '1,30p'
  4. 18s railway api --help 2>&1 | head -25
  5. 22s railway api 'mutation { serviceUpdate(id: "d837b58f-00ff-4536-8b22-af630aa4ff2c", input: {name: "adb-sufaa-crud-db"}) { id name } }' 2>&1 | tail -3
  6. 25s railway service list --json 2>/dev/null; railway config migrate --help 2>&1 | head -25
  7. 31s rm railway.json && railway add --service adb-sufaa-crud-web --variables 'DATABASE_URL=${{adb-sufaa-crud-db.DATABASE_URL}}' 2>&1 | tail -3; railway service list --json 2>/dev/null | node -e 'for(const
  8. 36s railway api 'mutation { serviceInstanceUpdate(serviceId: "9cf891c8-b8a6-4bc7-862c-540f64e6a1dd", environmentId: "ba95e605-7be5-4d6c-868a-df23a7d8fcef", input: {healthcheckPath: "/healthz", healthcheck
  9. 41s railway variable list --service adb-sufaa-crud-web --json 2>/dev/null | node -e 'const u=new URL(JSON.parse(require("fs").readFileSync(0)).DATABASE_URL);console.log("db host:",u.hostname)'; railway up
  10. 1m 11s railway domain --service adb-sufaa-crud-web --json 2>&1 | grep -v -i 'config as code\|migrate\|keep working'; sleep 20; railway logs --service adb-sufaa-crud-web 2>&1 | tail -5
  11. 1m 41s python3 - <<'EOF'
  12. 1m 49s U=https://adb-sufaa-crud-web-production.up.railway.app; sed -i "s#http://localhost:3456/#$U/#" /tmp/e2e/e2e.js && node /tmp/e2e/e2e.js
  13. 1m 56s sleep 60; railway service list --json 2>/dev/null | node -e 'for(const s of JSON.parse(require("fs").readFileSync(0)))console.log(s.name,s.latestDeployment.status,s.latestDeployment.createdAt)'; U=htt
  14. 1m 59s railway service list --json 2>/dev/null | node -e 'for(const s of JSON.parse(require("fs").readFileSync(0)))console.log(s.name,s.latestDeployment.status,s.latestDeployment.createdAt)'
  15. 2m 03s until railway service list --json 2>/dev/null | grep -A3 '"adb-sufaa-crud-web"' | grep -q '"status": "SUCCESS"\|"status": "FAILED"\|"status": "CRASHED"'; do sleep 5; done; railway service list --json
  16. 2m 09s st(){ railway service list --json 2>/dev/null | node -e 'const s=JSON.parse(require("fs").readFileSync(0)).find(s=>s.name=="adb-sufaa-crud-web");console.log(s.latestDeployment.status)'; }; until [[ "$
  17. 2m 26s railway usage --json 2>&1 | grep -v -i 'config as code\|migrate\|keep working' | head -30
  18. 2m 43s cat > deploy.sh <<'EOF'
  19. 2m 49s python3 - <<'EOF'

Raw data