Canonical architecture reference. Decisions log lives in docs/decisions/. Blueprint in docs/archive/2026-Q2/plans/ideony-mvp0-blueprint.md.
Ideony is an Italian skilled-trades marketplace (plumbers, electricians, HVAC, locksmiths) connecting consumers with vetted professionals. Two operating modes: standard booking — consumer searches, reviews trust-scored professionals, and books a slot (ProntoPro / MioDottore pattern); and SOS dispatch — consumer triggers an emergency, the system broadcasts to nearby professionals via PostGIS radius expansion, the first to accept gets dispatched, and both parties track each other in real time (Uber/Bolt pattern). Payments flow through Stripe Connect Express: funds are held at booking and released after job completion.
Consumer["Consumer\n(iOS / Android)"]
Professional["Professional\n(iOS / Android)"]
Admin["Admin\n(Web dashboard)"]
API["Ideony API\n(NestJS + Fastify)"]
Clerk["Clerk\n(Auth / Identity)"]
Stripe["Stripe Connect\n(Payments)"]
Mapbox["Mapbox\n(Geocoding / Routing)"]
Novu["Novu Cloud EU\n(Notifications)"]
R2["Cloudflare R2\n(Object Storage)"]
Sentry["Sentry\n(Error Monitoring)"]
AI["OpenAI / Gemini\n(AI features)"]
Consumer -->|HTTPS + WS| API
Professional -->|HTTPS + WS| API
API -->|JWT verify / webhooks| Clerk
API -->|PaymentIntents / Connect| Stripe
API -->|Geocoding / Directions| Mapbox
API -->|Trigger workflows| Novu
API -->|Presigned PUT/GET| R2
API -->|Error events| Sentry
API -->|LangGraph chains| AI
Novu -->|Email| Resend["Resend\n(Email)"]
Novu -->|SMS| Twilio["Twilio\n(SMS)"]
Novu -->|Push| ExpoPush["Expo Push\n(Mobile)"]
subgraph Mobile ["apps/mobile — Expo SDK 55 / RN 0.78"]
MobileApp["Expo Router 5\nScreen stack"]
TQ["TanStack Query\n(server state)"]
Zustand["Zustand\n(client state)"]
SocketClient["socket.io-client"]
MapboxRN["@rnmapbox/maps"]
subgraph APILayer ["apps/api — NestJS 11 / Fastify 5"]
AuthMod["AuthModule\nClerkGuard / RolesGuard"]
ProfMod["ProfessionalsModule\n+ CredentialsModule"]
SearchMod["SearchModule\n(PostGIS radius)"]
SOSMod["SOSModule\n(dispatch engine)"]
ChatMod["ChatModule\n(WS Gateway)"]
ReviewsMod["ReviewsModule"]
PayMod["PaymentsModule\n(Stripe Connect)"]
NotifMod["NotificationsModule\n(Novu)"]
StoreMod["StorageModule\n(R2 / MinIO)"]
AIMod["AIModule\n(LangGraph)"]
OnboardMod["OnboardingModule"]
PriceMod["PricingModule"]
BullWorkers["BullMQ Workers\n(notifications / reminders / retries)"]
subgraph Data ["Data stores"]
PG["PostgreSQL 18\n+ PostGIS\n(primary DB)"]
Redis["Redis 8.6\n(cache / queues / pub-sub)"]
subgraph APIClient ["packages/api-client"]
SDK["Generated SDK\n(@hey-api/openapi-ts)"]
SocketClient -->|WebSocket| ChatMod
SocketClient -->|WebSocket| SOSMod
Layer Tech Version Purpose Runtime Expo ~55.0.0 Managed RN build + OTA updates Framework React Native ~0.78.0 Cross-platform UI Router Expo Router ~5.0.0 File-based navigation Styling NativeWind ^4.1.0 Tailwind CSS for RN → ADR-0009 Design System Gluestack UI v3 (NativeWind) Copy-paste component kit Icons phosphor-react-native ^3.0.4 Duotone nav icons Icons lucide-react-native ^1.8.0 Inline feature icons Auth @clerk/expo ^3.1.12 Social login + JWT → ADR-0005 Payments @stripe/stripe-react-native ^0.64.0 Stripe Elements Maps @rnmapbox/maps ^10.3.0 Custom Sole-palette map → ADR-0008 Server state @tanstack/react-query ^5.99.0 Fetch / cache / sync Client state zustand ^5.0.12 Auth, SOS, ephemeral UI Real-time socket.io-client ^4.8.3 SOS dispatch + chat Animations react-native-reanimated ~3.17.0 60fps transitions Animations lottie-react-native ^7.3.6 Illustration / loading i18n i18next + react-i18next ^26.0.5 / ^17.0.3 IT + EN Monitoring @sentry/react-native ~8.8.0 Crash reporting Testing jest-expo ~55.0.0 Unit + component tests E2E @playwright/test ^1.59.1 Preview tour screenshots TypeScript typescript ^5.8.0 Static typing
Layer Tech Version Purpose Framework NestJS ^11.1.0 DI / module system → ADR-0003 HTTP adapter Fastify ^5.0.0 High-throughput HTTP Transpiler SWC ^1.15.26 Fast TS compilation ORM Prisma ^7.7.0 Type-safe DB client → ADR-0017 DB adapter @prisma/adapter-pg ^7.7.0 PostgreSQL driver Queue BullMQ ^5.74.1 Async jobs (notifications, retries) Queue client ioredis ^5.4.0 Redis connection Auth backend @clerk/backend ^3.2.11 JWT verify + webhooks Payments stripe ^22.0.2 Connect Express + webhooks Notifications @novu/api ^3.15.0 EU-region Novu SDK Storage @aws-sdk/client-s3 ^3.1030.0 R2 / MinIO (S3-compatible) AI / agents @langchain/langgraph ^1.2 Agentic AI chains AI model (primary) @langchain/openai ^1.4 GPT-5.4 Mini AI model (fallback) @langchain/google-genai ^2.1 Gemini 2.5 Flash AI tracing langsmith ^0.5 LangSmith EU Validation zod + nestjs-zod ^3.24.0 / ^5.3.0 Schema validation (no class-validator) i18n nestjs-i18n ^10.6.1 IT + EN translations WebSocket @nestjs/websockets + socket.io ^11.1.0 / ^4.8.3 Real-time gateway Security @fastify/helmet ^13.0.0 HTTP security headers Monitoring @sentry/nestjs ^10.49.0 Error / performance tracing Testing vitest ^3.0.0 Unit + integration tests E2E @playwright/test ^1.59.1 Smoke tests TypeScript typescript ^6.0.0 Static typing
Layer Tech Version Purpose Monorepo Turborepo ^2.9.6 Task orchestration + remote cache Package manager pnpm 9.15.4 Workspace installs Linter / formatter Biome ^2.0.0 Replaces ESLint + Prettier Git hooks Husky + lint-staged ^9.0.0 / ^15.0.0 Pre-commit quality gate DB PostgreSQL 18 + PostGIS — Primary store + geo queries Cache / queues Redis 8.6 BullMQ + pub-sub + API cache Container Docker — Single-container deploy Hosting Dokploy on Hetzner CAX11 — ARM64, ~€5/mo → ADR-0012 IaC Pulumi TypeScript — Hetzner + Dokploy provisioning Object storage Cloudflare R2 — Media + credential docs CDN / tunnel Cloudflare — Quick tunnel → named tunnel CI/CD GitHub Actions — Lint → build → test → deploy Node node >=22.0.0 Runtime constraint
participant M as Mobile App
participant A as Ideony API
participant DB as PostgreSQL
C->>M: Search "plumber near me"
M->>A: GET /search?lat=&lng=&category=
A->>DB: PostGIS ST_DWithin radius query
DB-->>A: Ranked professionals
A-->>M: Search results + trust scores
C->>M: Select professional, pick slot
A->>DB: Create Booking (PENDING)
A->>S: PaymentIntent.create (capture_method=manual)
A-->>M: client_secret + bookingId
M->>S: confirmPayment (card / Apple Pay)
S-->>M: payment_intent.succeeded
A->>N: Trigger booking-request workflow
N-->>Pro: Push + email notification
Pro->>A: POST /bookings/:id/accept
A->>DB: Update Booking → ACCEPTED
A->>S: PaymentIntent.capture
A->>N: Trigger booking-confirmed workflow
N-->>C: Confirmation push + email
Note over C,N: Job completed
C->>A: POST /bookings/:id/complete
A->>S: Transfer to professional Connect account
A->>DB: Update Booking → COMPLETED
A->>N: Trigger payout-released workflow
participant M as Mobile App
participant A as Ideony API
participant DB as PostgreSQL
participant WS as WS Gateway
M->>A: POST /sos (lat, lng, category)
A->>DB: Create SOSRequest (SEARCHING)
A->>DB: PostGIS ST_DWithin 10km → candidate pros
A->>R: Cache candidates, set TTL
loop Cascade: 10km → 20km → 30km
A->>WS: Emit sos:dispatch to candidate pros
WS-->>Pro: Real-time dispatch alert
Pro->>A: POST /sos/:id/accept
A->>DB: atomic updateMany (prevent race)
A->>DB: Update SOSRequest → MATCHED
A->>WS: Emit sos:matched to consumer
WS-->>C: Professional en route
A->>MB: Directions API (pro → consumer)
Pro->>WS: Emit location:update
WS-->>C: Real-time pro position
A->>DB: Expand radius to next tier
Note over C,MB: Job completed → same payout flow as booking
subgraph GHA ["GitHub Actions CI/CD"]
CI["lint → typecheck → build\n→ unit tests → SAST/audit"]
CD["SSH deploy → Docker build\n→ prisma migrate → blue-green\n→ health check → Trivy → smoke"]
subgraph Hetzner ["Hetzner CAX11 ARM64 — 178.104.154.74"]
subgraph Docker ["Docker (--network host)"]
APIContainer["ideony-api\n:3001"]
PGLocal["PostgreSQL 18\n+ PostGIS\n:5432"]
RedisLocal["Redis 8.6\n:6379"]
APIContainer --> RedisLocal
subgraph CF ["Cloudflare"]
Tunnel["Quick Tunnel\n(→ named tunnel post-DNS)"]
R2Store["R2 Object Storage"]
subgraph External ["External SaaS"]
StripeSvc["Stripe Connect"]
LangSmithSvc["LangSmith EU"]
GHCR["ghcr.io\n(ARM64 image cache)"]
Hetzner -->|Pull image| GHCR
APIContainer --> ClerkSvc
APIContainer --> StripeSvc
APIContainer --> MapboxSvc
APIContainer --> SentrySvc
APIContainer --> LangSmithSvc
Mobile["Mobile App\n(iOS / Android)"] -->|HTTPS + WS| Tunnel
Monorepo boundary : shared code lives only in packages/; apps never import each other directly.
Single NestJS container : all modules in one process; BullMQ workers run in-process. No microservices for MVP 0.
Auth identity via JWT only : @CurrentUser() from verified Clerk JWT. User identity MUST NOT come from request body.
No class-validator / class-transformer : all validation via Zod + nestjs-zod (createZodDto, createZodValidationPipe).
Prices in EUR, stored as Decimal : never float. Stripe amounts in cents (integer).
Async ops via BullMQ : notifications, reminders, and webhook retries are never synchronous in the request path.
Immutable data : new objects always; no in-place mutation (enforced by code style rules).
Absolute imports only : no relative ../ paths across module boundaries.
Zero warnings policy : every linter, type-checker, and test warning must be resolved or explicitly suppressed with a justification comment.
Built-in over custom : Clerk components, Stripe Elements — never custom implementations unless vendor capability gap is documented.
PostGIS for all geo queries : ST_DWithin for radius search and SOS cascade; never haversine in application code.
SOS race condition : professional accept uses atomic updateMany with status = SEARCHING precondition to prevent double-dispatch.
SDK contract : mobile app consumes packages/api-client generated types — hand-written API calls are not permitted.
ARM64 target : Hetzner CAX11 is ARM64; Docker images must be built for linux/arm64.
No direct push to main : feature branches + PRs during stable phases; direct merge permitted during MVP 0 sprint only.
E2E tenancy isolation : User, Booking, Review carry a nullable testTenant text column (indexed). Prod rows stay NULL; E2E harness rows get a UUID stamped by TenantMiddleware so /test/cleanup can scope deletes. Cascade FKs handle descendant rows (ChatMessage, Credential, Dispatch, Quote, etc.). Migration: prisma/migrations/20260421130531_e2e_test_tenant_marker. See docs/specs/2026-04-21-e2e-strategy.md + ADR 0026 .