Skip to content

Ideony — Architecture Reference

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.


graph LR
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
Admin -->|HTTPS| 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)"]

graph TB
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"]
end
subgraph APILayer ["apps/api — NestJS 11 / Fastify 5"]
AuthMod["AuthModule\nClerkGuard / RolesGuard"]
UsersMod["UsersModule"]
ProfMod["ProfessionalsModule\n+ CredentialsModule"]
SearchMod["SearchModule\n(PostGIS radius)"]
BookMod["BookingModule"]
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)"]
end
subgraph Data ["Data stores"]
PG["PostgreSQL 18\n+ PostGIS\n(primary DB)"]
Redis["Redis 8.6\n(cache / queues / pub-sub)"]
end
subgraph APIClient ["packages/api-client"]
SDK["Generated SDK\n(@hey-api/openapi-ts)"]
end
MobileApp --> SDK
SDK -->|HTTPS| APILayer
SocketClient -->|WebSocket| ChatMod
SocketClient -->|WebSocket| SOSMod
APILayer --> PG
APILayer --> Redis
BullWorkers --> Redis
BullWorkers --> NotifMod

LayerTechVersionPurpose
RuntimeExpo~55.0.0Managed RN build + OTA updates
FrameworkReact Native~0.78.0Cross-platform UI
RouterExpo Router~5.0.0File-based navigation
StylingNativeWind^4.1.0Tailwind CSS for RN → ADR-0009
Design SystemGluestack UI v3(NativeWind)Copy-paste component kit
Iconsphosphor-react-native^3.0.4Duotone nav icons
Iconslucide-react-native^1.8.0Inline feature icons
Auth@clerk/expo^3.1.12Social login + JWT → ADR-0005
Payments@stripe/stripe-react-native^0.64.0Stripe Elements
Maps@rnmapbox/maps^10.3.0Custom Sole-palette map → ADR-0008
Server state@tanstack/react-query^5.99.0Fetch / cache / sync
Client statezustand^5.0.12Auth, SOS, ephemeral UI
Real-timesocket.io-client^4.8.3SOS dispatch + chat
Animationsreact-native-reanimated~3.17.060fps transitions
Animationslottie-react-native^7.3.6Illustration / loading
i18ni18next + react-i18next^26.0.5 / ^17.0.3IT + EN
Monitoring@sentry/react-native~8.8.0Crash reporting
Testingjest-expo~55.0.0Unit + component tests
E2E@playwright/test^1.59.1Preview tour screenshots
TypeScripttypescript^5.8.0Static typing
LayerTechVersionPurpose
FrameworkNestJS^11.1.0DI / module system → ADR-0003
HTTP adapterFastify^5.0.0High-throughput HTTP
TranspilerSWC^1.15.26Fast TS compilation
ORMPrisma^7.7.0Type-safe DB client → ADR-0017
DB adapter@prisma/adapter-pg^7.7.0PostgreSQL driver
QueueBullMQ^5.74.1Async jobs (notifications, retries)
Queue clientioredis^5.4.0Redis connection
Auth backend@clerk/backend^3.2.11JWT verify + webhooks
Paymentsstripe^22.0.2Connect Express + webhooks
Notifications@novu/api^3.15.0EU-region Novu SDK
Storage@aws-sdk/client-s3^3.1030.0R2 / MinIO (S3-compatible)
AI / agents@langchain/langgraph^1.2Agentic AI chains
AI model (primary)@langchain/openai^1.4GPT-5.4 Mini
AI model (fallback)@langchain/google-genai^2.1Gemini 2.5 Flash
AI tracinglangsmith^0.5LangSmith EU
Validationzod + nestjs-zod^3.24.0 / ^5.3.0Schema validation (no class-validator)
i18nnestjs-i18n^10.6.1IT + EN translations
WebSocket@nestjs/websockets + socket.io^11.1.0 / ^4.8.3Real-time gateway
Security@fastify/helmet^13.0.0HTTP security headers
Monitoring@sentry/nestjs^10.49.0Error / performance tracing
Testingvitest^3.0.0Unit + integration tests
E2E@playwright/test^1.59.1Smoke tests
TypeScripttypescript^6.0.0Static typing
LayerTechVersionPurpose
MonorepoTurborepo^2.9.6Task orchestration + remote cache
Package managerpnpm9.15.4Workspace installs
Linter / formatterBiome^2.0.0Replaces ESLint + Prettier
Git hooksHusky + lint-staged^9.0.0 / ^15.0.0Pre-commit quality gate
DBPostgreSQL 18 + PostGISPrimary store + geo queries
Cache / queuesRedis8.6BullMQ + pub-sub + API cache
ContainerDockerSingle-container deploy
HostingDokploy on Hetzner CAX11ARM64, ~€5/mo → ADR-0012
IaCPulumi TypeScriptHetzner + Dokploy provisioning
Object storageCloudflare R2Media + credential docs
CDN / tunnelCloudflareQuick tunnel → named tunnel
CI/CDGitHub ActionsLint → build → test → deploy
Nodenode>=22.0.0Runtime constraint

sequenceDiagram
actor C as Consumer
participant M as Mobile App
participant A as Ideony API
participant DB as PostgreSQL
participant S as Stripe
participant N as Novu
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
M->>A: POST /bookings
A->>DB: Create Booking (PENDING)
A->>S: PaymentIntent.create (capture_method=manual)
S-->>A: client_secret
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

sequenceDiagram
actor C as Consumer
participant M as Mobile App
participant A as Ideony API
participant DB as PostgreSQL
participant R as Redis
participant WS as WS Gateway
participant MB as Mapbox
C->>M: Tap SOS button
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
alt Professional accepts
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)
MB-->>A: Polyline route
loop Live tracking
Pro->>WS: Emit location:update
WS-->>C: Real-time pro position
end
else Timeout at tier
A->>DB: Expand radius to next tier
end
end
Note over C,MB: Job completed → same payout flow as booking

graph TB
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"]
CI --> CD
end
subgraph Hetzner ["Hetzner CAX11 ARM64 — 178.104.154.74"]
subgraph Docker ["Docker (--network host)"]
APIContainer["ideony-api\n:3001"]
end
PGLocal["PostgreSQL 18\n+ PostGIS\n:5432"]
RedisLocal["Redis 8.6\n:6379"]
APIContainer --> PGLocal
APIContainer --> RedisLocal
end
subgraph CF ["Cloudflare"]
Tunnel["Quick Tunnel\n(→ named tunnel post-DNS)"]
R2Store["R2 Object Storage"]
end
subgraph External ["External SaaS"]
ClerkSvc["Clerk (EU)"]
StripeSvc["Stripe Connect"]
NovuSvc["Novu Cloud EU"]
MapboxSvc["Mapbox APIs"]
SentrySvc["Sentry"]
LangSmithSvc["LangSmith EU"]
GHCR["ghcr.io\n(ARM64 image cache)"]
end
CD -->|SSH| Hetzner
CD -->|Push image| GHCR
Hetzner -->|Pull image| GHCR
Tunnel --> APIContainer
APIContainer --> R2Store
APIContainer --> ClerkSvc
APIContainer --> StripeSvc
APIContainer --> NovuSvc
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.

DocumentPath
MVP 0 Blueprintdocs/archive/2026-Q2/plans/ideony-mvp0-blueprint.md
UX/UI Overhaul Plandocs/archive/2026-Q2/plans/ideony-ux-overhaul-plan.md
Roadmap + Decisions Logdocs/roadmap.md + docs/decisions/
ChangelogCHANGELOG.md
Project InstructionsCLAUDE.md
ADRsdocs/adr/ (pending extraction)
Shared Decisions Dochttps://docs.google.com/document/d/1tyv9Lyad3nZzeM_tLFsLdmGmS1WL7PexOMtJlTBmiuw