Data flow
Request lifecycle (web)
Section titled “Request lifecycle (web)”sequenceDiagram
participant B as Browser
participant SK as SvelteKit Server
participant BA as Better Auth
participant DB as PostgreSQL
participant VK as Valkey
participant MS as Meilisearch
participant IP as imgproxy
B->>SK: HTTP Request
SK->>BA: Validate session cookie
BA-->>SK: Session + User
SK->>DB: Kysely query (property data)
SK->>VK: GEOSEARCH (property search)
SK->>MS: Location autocomplete
SK->>IP: Sign image URLs (HMAC)
SK-->>B: SSR HTML + hydration payload
hooks.server.ts validates the Better Auth session cookie on every request and populates event.locals.user / event.locals.session. Protected routes (/portal/*, /dashboard/*) check locals.user in +layout.server.ts.
Property search pipeline
Section titled “Property search pipeline”- User enters a location → Meilisearch provides autocomplete suggestions.
- Map viewport changes → client sends bounds to server.
- Server calls
@repo/redis-search→ Valkey GEOSEARCH returns property IDs within the viewport. - Additional filters (price, beds, type) are applied in-memory.
- Property IDs fetch full records from PostgreSQL via Kysely.
- Image URLs are HMAC-signed via imgproxy and returned.
The price filter is multi-unit aware — overlap check against ZIDX_PRICE + ZIDX_MAX_PRICE so any unit’s price falling within the range qualifies the property.
Image pipeline
Section titled “Image pipeline”flowchart LR
Upload["Client upload"] --> API["SvelteKit API route"]
API --> S3["Cloudflare R2"]
S3 --> imgproxy["imgproxy (HMAC)"]
imgproxy --> T["Thumb 400×300"]
imgproxy --> M["Medium 800×600"]
imgproxy --> L["Large 1200×800"]
- Uploads go through a SvelteKit API route to Cloudflare R2 via
@aws-sdk/client-s3. - imgproxy serves images on-demand: resize, WebP conversion, quality optimization.
- URLs are HMAC-signed server-side — attackers cannot manipulate dimensions or request arbitrary resources.
Background jobs (BullMQ)
Section titled “Background jobs (BullMQ)”The standalone worker at apps/worker/ consumes five queues:
| Queue | Jobs |
|---|---|
search-indexing | sync-property (2s debounce, 3 retries) → Valkey (includes priceDropPercent from price_history); sync-agent, sync-team → Meilisearch |
leads | lead-ingestion (reliable Inquiry creation with DLQ to failed_lead_ingestions); sla-monitor (repeatable */15 * * * *) |
alerts | price-drop-notify, status-change-notify (event-driven); saved-search-scan (repeatable */30 * * * *) |
ai | generate-description (Gemini 2.5 Flash); one-release compatibility for legacy staging entries |
ai-staging | Resumable stage-room predictions through Replicate’s official openai/gpt-image-2 wrapper |
Every queue name (including the Bull Board registration) is suffixed with process.env.QUEUE_SUFFIX. Production leaves it empty; local dev sets -dev so jobs enqueued from the dev svelte-web don’t get stolen by deployed workers sharing the same Valkey.
Room staging is isolated from descriptions. ai-staging consumes payloads containing only { aiJobId }, uses that ID as the stable BullMQ ID, and reloads all canonical state from PostgreSQL. A startup/30-second reconciler redispatches missing PENDING, RETRYING, or cancel-requested work. AI_STAGING_CONCURRENCY and AI_STAGING_RATE_LIMIT_PER_MINUTE tune only staging; provider attempts are separately persisted and capped at two paid predictions per logical job.
Mobile flow
Section titled “Mobile flow”flowchart LR
Mobile["Mobile (Expo + native iOS/Android)"] -->|"Cookie: header"| API["apps/api (Hono)"]
API -->|tRPC| Service["@repo/api services"]
API -->|Better Auth| Postgres["PostgreSQL"]
Service --> Postgres
Service --> Valkey["Valkey"]
The native iOS/Android apps talk to a single host (apps/api) which mounts both /api/auth/* (Better Auth) and /api/trpc/* (tRPC fetch adapter). They use a hand-written tRPC-over-HTTP client (SuperJSON envelope, no batching; response models generated from @repo/contract). Auth state lives as a cookie — in the Keychain (iOS) / EncryptedSharedPreferences (Android) — never a Bearer token. No CSRF token is sent: mobile requests carry no Origin and are exempt from the server’s Origin allowlist.
See Conventions → Mobile (Native) for the rationale and gotchas.