Architecture¶
System Overview¶
Retailment Marketplace is a multi-component platform consisting of three main services:
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Public Website │ │ Backend API │ │ Admin App │
│ (TanStack Start) │────▶│ (Express.js) │◀────│ (React Native) │
│ Port: 3000 │ │ Port: 5000 │ │ Port: 19006 │
└─────────────────────┘ └──────────┬──────────┘ └─────────────────────┘
│
▼
┌─────────────────────┐
│ PostgreSQL 15 │
│ (Docker) │
│ Port: 5432 │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Telegram API │
│ (Syndication) │
└─────────────────────┘
Backend Architecture¶
The backend follows a modular monolith pattern with domain-based organization:
src/
├── app.js # Express app setup, middleware, routes
├── server.js # Server bootstrap, DB connection
├── config/
│ └── prisma.js # Prisma client singleton
├── core/
│ └── listingEmitter.js # Central event bus (EventEmitter)
├── modules/
│ ├── users/ # Auth, registration, profile
│ ├── properties/ # Categories, listings, public API
│ ├── syndication/ # Telegram integration
│ ├── notifications/ # In-app notifications
│ └── commissions/ # Admin revenue tracking
└── utils/
├── imageProcessor.js # Sharp-based image pipeline
├── cloudinary.js # Cloud storage abstraction
└── brevo.js # Email service
Key Architectural Patterns¶
1. Event-Driven Syndication¶
Listing events trigger automatic Telegram posting via a centralized event emitter:
// backend/src/core/listingEmitter.js:1-7
import { EventEmitter } from 'events';
const listingEmitter = new EventEmitter();
listingEmitter.setMaxListeners(20);
export default listingEmitter;
Events emitted:
- listing:created - New listing created
- listing:updated - Listing modified
- listing:deleted - Listing removed
2. Dynamic Validation¶
Categories define schemaRules (JSON) that are validated against listing attributes at runtime:
// backend/src/modules/properties/dynamic.validation.js:49-96
export function validateDynamicAttributes(attributes, schemaRules) {
// Validates and sanitizes attributes based on category rules
// Supports: number, string, boolean, date types
}
3. Middleware Pipeline¶
Listing creation/update follows a strict middleware pipeline:
// backend/src/modules/properties/listing.routes.js:18-23
const protectedWriteMiddleware = [
authenticate(), // JWT verification
processImages, // Multer file upload
optimizeImages, // Sharp resize + WebP conversion
validateListingAttributes(), // Dynamic schema validation
];
4. Dual Storage Backend¶
Images can be stored locally or on Cloudinary, configurable via environment:
// backend/src/utils/imageProcessor.js:148-153
async function processSingleImage(file) {
if (STORAGE_PROVIDER === 'cloudinary') {
return processSingleImageCloudinary(file);
}
return processSingleImageLocal(file);
}
Data Flow¶
Listing Creation Flow¶
Client Request (multipart/form-data)
│
▼
┌─────────────────┐
│ authenticate() │ ── JWT verification ──▶ req.user
└────────┬────────┘
│
▼
┌─────────────────┐
│ processImages │ ── Multer upload to temp files
└────────┬────────┘
│
▼
┌─────────────────┐
│ optimizeImages │ ── Sharp resize (1200px) + WebP conversion
└────────┬────────┘
│
▼
┌─────────────────────────┐
│ validateListingAttributes│ ── Dynamic schema validation
└────────┬────────────────┘
│
▼
┌─────────────────┐
│ createListing │ ── Prisma create + emit event
└────────┬────────┘
│
├──▶ Database (PostgreSQL)
│
└──▶ listingEmitter.emit('listing:created')
│
▼
┌─────────────────────┐
│ telegramListener │ ── Async Telegram syndication
└─────────────────────┘
Telegram Syndication Flow¶
listing:created / listing:updated event
│
▼
┌─────────────────────┐
│ handleSyndication() │
│ (telegramListener) │
└────────┬────────────┘
│
├── On update: Check for existing message
│ ├── Found? → Try editMessageCaption()
│ │ ├── Success → Log SUCCESS
│ │ └── Failed → Fall through to new post
│ └── Not found → Create new post
│
└── Create new post
│
▼
┌─────────────────────┐
│ sendListingToChannel│
│ (telegramBot.service)│
└────────┬────────────┘
│
├── 1 image → sendPhoto()
└── 2+ images → sendMediaGroup()
│
▼
Telegram API
Frontend Architecture¶
Public Website (TanStack Start)¶
src/
├── routes/
│ ├── __root.tsx # Root layout with providers
│ ├── index.tsx # Homepage (hero, categories, featured)
│ ├── catalog.tsx # Search/filter page
│ └── property.$id.tsx # Property detail page
├── components/
│ ├── Nav.tsx # Desktop + mobile navigation
│ ├── PropertyCard.tsx # Listing card with skeleton
│ ├── FilterPanel.tsx # Search filters
│ └── ui/ # 46 Radix-based UI primitives
├── hooks/ # Custom React hooks
├── lib/
│ ├── api.ts # API client (fetch wrapper)
│ └── i18n.ts # Internationalization
└── providers/
├── theme.tsx # Dark/light mode
└── locale.tsx # Language context (EN/OM/AM)
Provider Hierarchy:
QueryClientProvider (TanStack React Query)
└── ThemeProvider (dark/light mode)
└── LocaleProvider (language context)
└── <Outlet /> (route content)
Admin App (React Native)¶
src/
├── navigation/
│ └── MainNavigator.js # Bottom tabs + stack navigators
├── screens/ # 14 screens
│ ├── LoginScreen.js
│ ├── DashboardScreen.js
│ ├── PropertiesScreen.js
│ ├── AddListingScreen.js
│ └── ...
├── components/ # Reusable UI components
├── context/
│ ├── AuthContext.js # JWT token management
│ ├── ThemeContext.js # Dark/light mode
│ └── LanguageContext.js # i18n
├── api/
│ ├── client.js # Axios instance with interceptors
│ ├── auth.js # Auth API calls
│ ├── listings.js # Listing CRUD
│ └── ...
└── i18n/ # Translation files
Navigation Structure:
BottomTabNavigator
├── DashboardTab (stack)
├── PropertiesTab (stack)
├── SyndicationTab (stack)
├── CommissionsTab (stack, ADMIN only)
└── SettingsTab (stack)
Database Schema¶
See Database Schema for detailed model definitions.
Entity Relationship¶
User ──1:N──▶ Listing ──1:N──▶ SyndicationLog
│ │
│ └──N:1──▶ Category
│
└──1:N──▶ Notification
SyndicationConfig (standalone)
Error Handling¶
The backend implements a centralized error handler in app.js:71-108:
- DynamicValidationError → 422 with field-level details
- Prisma P2002 (unique constraint) → 409 Conflict
- Prisma P2025 (record not found) → 404 Not Found
- MulterError → 400 Bad Request
- Generic errors → 500 (message hidden in production)