Browser UI Agent CLI
| |
+------ HTTPS API Gateway ------+
|
Railway App/API Service
|
+---------+----------+
| |
Railway Postgres S3-compatible
+ Graphile Worker object storage
| |
+---------+----------+
|
Railway Worker Services
http / intel / browser roles
|
httpx, nuclei, subfinder
Responsibilities:
- Better Auth browser sessions and admin/user management
- scan submission
- history and search queries
- API key management
- recurring scan schedule management
- SSE event fan-out from committed records
- Zod-backed validation at all HTTP boundaries
- screenshot and favicon proxy routes
Responsibilities:
- claim Graphile Worker jobs
- run
httpx,nuclei,subfinder, browser fallback, and IP intelligence phases - normalize scanner output into persisted scan records
- write
scan_attempts,scan_phase_runs,scan_results,scan_events, nuclei matches, detections, and schedule run state - honor cancellation requests
- stay private on Railway and never act as a public HTTP surface
Worker roles are selected with STACKRAY_WORKER_ROLE:
http:http_probeand legacyrun_scanbrowser:headlessandbrowser_fallbackintel:subfinder,nuclei_dns,nuclei_http,ip_intel,finalize, andschedule_due_scansall: every task, mostly useful for simple deployments and local checks
Source of truth for:
- users and auth/session tables
- scan requests
- attempts
- phase runs
- results
- event history
- API keys
- schedules and schedule dispatch history
- Graphile Worker jobs
Graphile Worker runs from the same Postgres database. Queue jobs are orchestration state, while scans, phases, results, schedules, and events remain the durable product records.
Queue messages contain lightweight identifiers such as:
scanIdattemptIdresultId
Workers must reload the canonical scan definition from Postgres.
Screenshots are stored outside Postgres in S3-compatible object storage. Local development uses MinIO; hosted deployments use Railway Object Storage or another S3-compatible bucket.
The database stores screenshot object keys and metadata. The app exposes authenticated screenshot routes that fetch from storage instead of exposing bucket URLs directly.
- deploy the app and worker as separate Railway services
- do not expose the worker service publicly
- split worker services by
STACKRAY_WORKER_ROLEfor production-like deployments - enforce outbound egress policies if possible
- rate limit public scan creation endpoints
- keep Postgres as the database and Graphile Worker job store
- configure S3-compatible object storage for screenshots
- use one public app domain for the browser UI, API, and SSE routes
Stackray intentionally uses standard HTTP/JSON plus SSE instead of tRPC or oRPC in v1.
Reasons:
- the browser UI, agent CLI, and internal worker ecosystem all need a stable non-TypeScript-specific contract
- SSE is already a first-class transport for scan progress
- explicit route and event contracts are easier to debug across Railway services and Graphile Worker jobs
Use scanner binaries from the worker image and persist each phase independently:
http_probe: primaryhttpxprobe and result selectionsubfinder: subdomain enrichmentheadless: runtime technology detection and screenshot capturebrowser_fallback: recovery pass for blocked or missing browser artifactsnuclei_dnsandnuclei_http: DNS, SSL, RDAP, robots, and technology evidenceip_intel: IP and routing enrichmentfinalize: terminal scan status and aggregate events
- UI or CLI submits scan.
- API normalizes targets and stores canonical request.
- API validates the request with Zod and enqueues a Graphile
http_probejob. - The HTTP worker claims the job, creates an attempt, runs
httpx, selects the authoritative result, and queues enrichment phases. - Browser and intelligence workers run their phase jobs and write structured findings, screenshots, detections, and events into Postgres.
- Finalization marks the scan terminal only after persisted phase state has settled.
- API exposes persisted normalized results plus raw evidence to UI and CLI over HTTP/JSON + SSE.
- Comparison, targets, schedules, and search operate over stored history.
- treat all scan input as untrusted
- default deny private IP and localhost targets
- make "scan private network" an explicit privileged policy later
- keep browser auth and user administration in Better Auth
- audit every scan request with actor and source metadata
- keep product-resource routes on the shared session-or-bearer actor boundary
- keep account, API-key, user-management, and product-state routes session-only