Dev environment (Railway)
DEV has completed its migration from Hostinger/Dokploy to Railway. Cloudflare remains the
DNS edge and Cloudflare R2 bucket uploads-dev remains the object store. Hostnames follow the
hostname scheme: DEV is the zone
dev.useast.kottia.com, with api., assets. and docs. under it. Staging and
production remain planned for GCP us-central1 and are not provisioned; see
Production landscape.
Topology
Section titled “Topology”Cloudflare DNS (zone kottia.com) dev.useast.kottia.com → svelte-web api.dev.useast.kottia.com → api assets.dev.useast.kottia.com → imgproxy docs.dev.useast.kottia.com → docs (reserved; DEV only, Cloudflare Access first) bull.dev.useast.kottia.com → Bull Board (optional; Basic auth required)
Railway kottia.com / development GitHub: svelte-web · api · worker · docs Images: imgproxy · postgres · valkey · meilisearch Private DNS: <service>.railway.internal
Off-platform: Cloudflare R2 uploads-dev · Resend · Google · Knock · GetStream · Sentry · PostHog · Replicate · Gemini · Stripe · FacturAPIPostgreSQL, Valkey, and Meilisearch have no public domains. They are reachable only through Railway private networking.
Services
Section titled “Services”The four app services build from real-estate-core/real-estate-core-mono, branch development.
Their Docker builds use the repository root as context and retain the existing Dockerfiles.
| Service | Dockerfile / image | Domain | Watch paths |
|---|---|---|---|
svelte-web | apps/svelte-web/Dockerfile | dev.useast.kottia.com | apps/svelte-web/**, packages/**, package.json, bun.lock |
api | apps/api/Dockerfile | api.dev.useast.kottia.com | apps/api/**, packages/**, package.json, bun.lock |
worker | apps/worker/Dockerfile | none by default | apps/worker/**, packages/**, package.json, bun.lock |
docs | apps/docs/Dockerfile | none yet — docs.dev.useast.kottia.com reserved, Access-gated | apps/docs/**, packages/**, package.json, bun.lock |
imgproxy | ghcr.io/imgproxy/imgproxy:v3.30.1 | assets.dev.useast.kottia.com | — |
postgres | postgis/postgis:16-3.5 | private | — |
valkey | valkey/valkey:8 | private | — |
meilisearch | getmeili/meilisearch:v1.9.0 | private | — |
Persistent volumes mount at:
- PostgreSQL:
/var/lib/postgresql/data - Valkey:
/data - Meilisearch:
/meili_data
Configuration as code
Section titled “Configuration as code”Railway deprecated the migration’s earlier per-service railway.json Config as Code. The current
supported repository configuration is project-wide .railway/railway.ts, using the railway
TypeScript SDK. Do not document or add railway.json as current configuration.
From the repository root:
railway config pullrailway config plan# Review every proposed service, domain, variable, and volume change.railway config applyApply only an explicitly reviewed plan. Values represented by preserve() remain managed in
Railway rather than stored in source.
Networking and variables
Section titled “Networking and variables”Use Railway service references for non-secret dependencies; do not duplicate resolved credentials across services. Consumers resolve the data services through:
postgres.railway.internalvalkey.railway.internalmeilisearch.railway.internalThe Valkey service enables requirepass, owns the credentialed VALKEY_URL, and exports that URL
to web, API, and worker through ${{valkey.VALKEY_URL}}. Generate VALKEY_PASSWORD with
openssl rand -hex 32; the value must remain URL-safe and shell-safe because it is embedded in
the URL and passed through VALKEY_EXTRA_FLAGS.
DEV’s required shared settings include:
QUEUE_SUFFIX=-devCOOKIE_DOMAIN=.dev.useast.kottia.comORIGIN=https://dev.useast.kottia.comThe API uses:
CORS_ORIGINS=https://dev.useast.kottia.comBETTER_AUTH_URL_API=https://api.dev.useast.kottia.comTRUSTED_PROXY_CIDRS=100.64.0.0/10 is currently configured as Railway’s internal proxy range.
That is not yet a permanent assumption: verify the socket peer seen by web and API and prove that
forged forwarding headers sent directly to each Railway-provided domain cannot alter client-IP
resolution.
R2 and imgproxy
Section titled “R2 and imgproxy”The existing Cloudflare R2 wiring remains:
AWS_S3_BUCKET_NAME=uploads-devAWS_DEFAULT_REGION=autoIMGPROXY_URL=https://assets.dev.useast.kottia.comKeep the existing R2 endpoint and credentials plus the matching IMGPROXY_KEY /
IMGPROXY_SALT. The worker also reads the existing search-seeds/*.ndjson.gz artifacts from R2.
assets.dev.useast is a DNS prerequisite, not decoration: it must be a Cloudflare CNAME (zone
kottia.com) to the *.up.railway.app target Railway shows for the domain on the imgproxy
service, with the TXT ownership record Railway lists next to it. The DEV/STG hosts are two or three
labels below the apex, which Cloudflare’s free Universal SSL does not cover — keep them DNS-only
(Railway’s Let’s Encrypt cert terminates TLS) unless the zone has Advanced Certificate Manager, in
which case proxying requires SSL/TLS mode Full. The record was never recreated when imgproxy moved off Dokploy, and
every image on DEV was broken for weeks (PRO-18). When it is missing or unverified, both
/api/images/* proxies answer 502 image_service_unavailable and log
[images] imgproxy upstream unreachable, while avatars and team logos — loaded from signed
imgproxy URLs directly in the browser — just fail to render. Confirm with
dig +short assets.dev.useast.kottia.com and
curl https://assets.dev.useast.kottia.com/health.
PUBLIC_API_URL is build-time
Section titled “PUBLIC_API_URL is build-time”Set PUBLIC_API_URL=https://api.dev.useast.kottia.com on svelte-web. Railway passes service
variables into Docker builds for matching declared ARGs, so the value reaches the existing
Dockerfile as a build argument. It is baked into the browser bundle; rebuild svelte-web after
changing it. A runtime-only update cannot repair an already-built bundle.
Secrets
Section titled “Secrets”Integration secrets were manually copied from Dokploy to the corresponding Railway services and
remain out of source. PII_ENCRYPTION_KEY must be copied verbatim: it decrypts persisted fiscal
PII and cannot be re-issued. An escrowed copy must exist outside both platforms, and the owner must
verify that escrow before the Hostinger VPS is cancelled. No escrow location is asserted here.
Initialize fresh data services
Section titled “Initialize fresh data services”Railway PostgreSQL is fresh: run migrations only, with no Dokploy data restore. Open a shell in the deployed worker or API container:
cd /app/packages/database && bun run scripts/migrate.tsKysely tracks applied migrations, so the command is idempotent.
Meilisearch is also fresh. The worker restores its location and service-area indexes at startup
from the existing R2 search-seeds/*.ndjson.gz artifacts. Do not delete that prefix during DEV
cleanup.
Deploy and verify
Section titled “Deploy and verify”Railway’s GitHub integration auto-deploys development. Each app watches its app path,
packages/**, and the root package.json and bun.lock; GitHub Actions remain checks only.
The docs service is DEV-only and its hostname docs.dev.useast.kottia.com is reserved, not yet
declared. Before exposing it, configure an enforceable Cloudflare Access/SSO policy (which requires
the record to be proxied — see the TLS note above). An
anonymous request must return 401/403 or a 30x whose Location points to the configured
Cloudflare Access login host; reject any 200 response containing the docs body or any response
carrying X-Railway-Edge, because either proves the request reached Railway without Access. Only
then add the domain to Railway and .railway/railway.ts.
When removing docs exposure, delete or park the Cloudflare docs.dev.useast DNS record first and verify
the hostname no longer resolves. Only then delete the Railway custom domain; this prevents a
dangling DNS record from becoming a subdomain-takeover surface.
- Web loads and hydrates at
dev.useast.kottia.com. - Email OTP and Google OAuth work.
- Browser tRPC returns JSON from
api.dev.useast.kottia.com/api/trpc/*. - Cross-subdomain cookies work and API CORS accepts only the web origin.
- Direct Railway-domain requests cannot spoof the resolved client IP.
- Search data restores from R2 seed artifacts.
- Uploads land in
uploads-devand render throughassets.dev.useast.kottia.com. -
assets.dev.useast.kottia.comresolves and its/healthreturns 200. -
GET /api/images/...for a real photo returns 200, not 502. - Worker jobs use
QUEUE_SUFFIX=-dev. - Pushes rebuild only services selected by watch paths.
- PostgreSQL, Valkey, and Meilisearch have mounted volumes and no public domains.
- The docs service has no Railway custom domain until
docs.dev.useast.kottia.comis verified behind Cloudflare Access. - No
productionor futurestagingservice carries a docs hostname.
Bull Board is sensitive: it grants queue control and exposes lead PII. Keep it internal by default;
if bull.dev.useast.kottia.com is enabled, BULL_BOARD_USER and BULL_BOARD_PASSWORD are
mandatory.
Rollback window
Section titled “Rollback window”Dokploy applications stay stopped while the Hostinger VPS remains available for about one week. Do not run both platforms as simultaneous active DEV writers.
Cancel Hostinger and remove the Dokploy GitHub App only after Railway has passed verification,
remained stable through the observation window, and the owner has confirmed the
PII_ENCRYPTION_KEY escrow outside both platforms.