Skip to content

Infrastructure

All development services run on a single Hostinger VPS under Dokploy (Docker Swarm + Traefik + Let’s Encrypt), fronted by Cloudflare. Only the public-facing hosts (web, apps/api, imgproxy, docs) have public domains; the data services live on the internal dokploy-network with no published ports. Object storage is Cloudflare R2 (off-box, S3-compatible). Staging + production target GCP us-central1 (Iowa) — decided, not yet provisioned; see Production landscape. This VPS is kept as the DEV tier under that plan.

flowchart TB
    subgraph Public["Public internet"]
        Browser["Browser"]
        Mobile["Mobile (native)"]
        R2[("Cloudflare R2 (S3)")]
    end

    subgraph Dokploy["Dokploy on Hostinger VPS (dokploy-network)"]
        SvelteKit["apps/svelte-web (Node + adapter-node)"]
        API["apps/api (Bun + Hono)"]
        Worker["apps/worker (Bun + BullMQ)"]
        DB[("PostgreSQL + PostGIS 16")]
        VK[("Valkey")]
        MS[("Meilisearch v1.9")]
        IP["imgproxy v3.30"]
    end

    Browser -->|HTTPS| SvelteKit
    Browser -->|HTTPS| API
    Browser -->|HTTPS, signed| IP
    Mobile -->|HTTPS| API
    SvelteKit --> DB
    SvelteKit --> VK
    SvelteKit --> MS
    SvelteKit --> R2
    API --> DB
    API --> VK
    Worker --> DB
    Worker --> VK
    Worker --> MS
    R2 --> IP
ServiceInternal URL patternPublic URL
PostgreSQLpostgresql://...@postgres:5432/app (dokploy-network)
Valkeyredis://:<pw>@valkey:6379 (dokploy-network)
Meilisearchhttp://meilisearch:7700 (dokploy-network)
Cloudflare R2https://<ACCOUNT_ID>.r2.cloudflarestorage.com (S3 API, off-box)
SvelteKit webhttps://dev.real-estate-core.com
apps/apihttps://api-dev.real-estate-core.com
imgproxyhttps://dev-assets.real-estate-core.com
docshttps://docs-dev.real-estate-core.com
Bull BoardInternal only — Dokploy app terminal / SSH tunnel (optional bull-dev domain behind Basic auth). DEV/STG only — PRD has no Bull Board; use bun run queue:admin (see Incident guides)
ServiceWhy it exists
PostgreSQL + PostGISSystem of record. 45 models, 85+ RLS policies. PostGIS for property ST_MakePoint coordinates.
Valkey(a) Property search via @repo/redis-search GEOSEARCH; (b) BullMQ queue backend (db1); (c) rate-limit buckets; (d) area price funnel cache.
MeilisearchLocations (SEPOMEX-sourced municipalities, cities, localities), agents, agent service areas. Fuzzy + filterable text search.
Cloudflare R2All uploaded user content: property images, floor plans, avatars. S3-compatible, zero egress fees.
imgproxyOn-demand resize + WebP conversion. HMAC-signed URLs — attackers can’t manipulate dimensions or request arbitrary resources.
apps/svelte-webPublic site + dashboards. SSR + form actions. Hosts its own Better Auth handler for the web flow.
apps/apiStandalone tRPC + Better Auth host for mobile (and eventually browser). Same DB + auth tables, interchangeable sessions.
apps/workerBullMQ consumer. Search indexing, lead ingestion, alerts, AI jobs (Gemini, Replicate).
QueueConcurrencyNotes
search-indexingdefaultsync-property (2s debounce, 3 retries) → Valkey; sync-agent, sync-team → Meilisearch
leadsdefaultlead-ingestion (reliable, DLQ to failed_lead_ingestions); sla-monitor (repeatable */15 * * * *)
alertsdefaultprice-drop-notify, status-change-notify, saved-search-scan (repeatable */30 * * * *)
ai1limiter: { max: 6/min } to stay under Replicate’s per-account cap at $0 credit

Every queue name is suffixed with process.env.QUEUE_SUFFIX. Production leaves it empty; local dev sets -dev. The Bull Board registration is suffix-aware too.

ToolCoverage
SentryError tracking + session replay, one project per deployable: apps/svelte-web (server + client), apps/api, apps/worker, and the native mobile apps (ios-user / android-user / ios-agent / android-agent, org na-v1f). The non-mobile inits run a beforeSend scrubber (redacts cookies, authorization, CSRF, `password
PostHogConsent-gated product analytics.
Bull BoardBullMQ dashboard — reach it via the Dokploy worker terminal or an SSH tunnel (or an optional Basic-auth domain) to inspect queue state, retry / replay failed jobs. DEV/STG only; in PRD the same operations are scripted via bun run queue:admin (Incident guides).
Knock dashboardInspect in-app feed deliveries + email channel state per workflow.
ScenarioMitigation
Lost Postgres passwordDokploy → data-services Compose → Environment (POSTGRES_PASSWORD) — reset requires re-initializing the volume or an ALTER USER via the container terminal. Update DATABASE_URL + DIRECT_URL in all apps.
Stale Valkey stateredis-cli -u "$VALKEY_URL" FLUSHDB then restart the worker so re-indexing fires.
Stale Meilisearch indexRe-run scripts/sync-locations.ts + scripts/enrich-viewports.ts + scripts/sync-service-areas.ts (see Dev environment); agents/teams re-index via the worker’s search-indexing queue.
imgproxy SSRFAlready mitigated: URLs are HMAC-signed server-side. IMGPROXY_KEY / IMGPROXY_SALT rotation invalidates all in-flight URLs.
Replicate traffic or spend spikeSet AI_ROOM_STAGING_ENABLED=false on web, API, and worker to stop new predictions; do not delete persisted jobs. Inspect the dedicated ai-staging queue and ai_provider_attempts, then adjust AI_STAGING_RATE_LIMIT_PER_MINUTE only against the current account limits. Existing prediction IDs remain resumable/cancelable.
DB schema driftcd packages/database && bun run db:reset — drops the schema, then re-runs the Kysely migration chain to latest (0001 ID-generator functions → 0002 baseline schema → 0003 triggers, then the incremental migrations after it) so generated-ID columns and triggers come back intact. See Architecture → Database.