AdSpot Platform — Architecture Diagrams
Complete visual reference for the AdSpot DOOH advertising platform. Covers user journeys, technical modules, and all key system flows.
2 Flow Diagrams
4 Sequence Diagrams
Draw.io source files available in /docs/diagrams/
flow-overview.md
flowchart TD
V(["🌐 Visitor"])
LP["Landing Page"]
REG["Register Account"]
LOG["Login"]
EV["Email Verification"]
ROLE{"Role?"}
V --> LP
LP --> REG
LP --> LOG
REG --> EV
EV --> ROLE
LOG --> ROLE
ROLE -->|ADMIN| ADM_DASH
ROLE -->|ADVERTISER| ADV_DASH
ROLE -->|PUBLISHER| PUB_DASH
subgraph ADMIN ["🛡️ Admin Portal"]
ADM_DASH["Admin Dashboard"]
ADM_DASH --> UM["User Management"]
ADM_DASH --> SM["Screen Management"]
ADM_DASH --> CM["Campaign Oversight"]
ADM_DASH --> MA["Media Approval"]
ADM_DASH --> PS["Payment Settings"]
ADM_DASH --> PA["Platform Analytics"]
ADM_DASH --> DSP["DSP / Programmatic Ads"]
ADM_DASH --> LC["Language & Location Config"]
ADM_DASH --> EB["Early Bird & Branding"]
end
subgraph ADVERTISER ["📢 Advertiser Portal"]
ADV_DASH["Advertiser Dashboard"]
ADV_DASH --> BAL["Balance & Top-up"]
BAL --> CHKOUT["Payment Checkout"]
ADV_DASH --> MC["My Campaigns"]
MC --> CC["Create Campaign"]
CC --> MU["Upload Media / Creative Studio"]
CC --> SS["Select Screens & Schedule"]
ADV_DASH --> ADVANA["Campaign Analytics"]
ADV_DASH --> TH["Transaction History"]
end
subgraph PUBLISHER ["📺 Publisher Portal"]
PUB_DASH["Publisher Dashboard"]
PUB_DASH --> RS["Register Screens"]
PUB_DASH --> SR["Set Screen Rates"]
PUB_DASH --> EARN["Earnings & Payouts"]
PUB_DASH --> PUBANA["Screen Analytics"]
PUB_DASH --> USB["USB Export for Offline Screens"]
end
subgraph SCREEN ["🖥️ TV Screen App"]
HB["Heartbeat Check-in (60s)"]
HB --> FETCH["Fetch Active Campaigns"]
FETCH --> PLAY["Play Ad Creatives"]
PLAY --> SYNC["Sync View Analytics"]
SYNC --> HB
end
RS -.->|"Screen goes live (activation code)"| HB
CC -.->|"Campaign approved & scheduled"| FETCH
style ADMIN fill:#fff3e0,stroke:#ef6c00
style ADVERTISER fill:#e3f2fd,stroke:#1565c0
style PUBLISHER fill:#e8f5e9,stroke:#2e7d32
style SCREEN fill:#fce4ec,stroke:#880e4f
flow-architecture.md
flowchart TB
subgraph CLIENTS ["⬛ Client Layer"]
direction LR
BROWSER["React SPA\nVite + Ant Design + Redux Toolkit"]
TVAPP["TV / Screen App\nAndroid / Web player"]
end
subgraph NGINX_LAYER ["🔀 Reverse Proxy"]
NGINX["Nginx :80 / :443 SSL termination"]
end
subgraph API ["⚙️ NestJS API — Express :3001"]
direction TB
subgraph AUTH_USERS ["Auth & Identity"]
AUTH["AuthModule\nJWT · Passport · Bcrypt · Email verify"]
USERS["UsersModule\nAdmin · Advertiser · Publisher"]
end
subgraph CONTENT ["Content & Media"]
MEDIA["MediaModule\nMulter upload · Admin approval"]
end
subgraph AD_CORE ["Advertising Core"]
CAMPAIGNS["CampaignsModule\nCreate · Schedule · Status · M:N screens"]
SCREENS["ScreensModule\nRegistration · Search · Rate lookup"]
CHARGING["ChargingModule\nRate engine · Slot deductions · Publisher credit"]
PROGRAMMATIC["ProgrammaticModule\nDSP · RTB bidding · Impressions"]
end
subgraph DELIVERY ["Screen Delivery"]
TVMOD["TvAppModule\nHeartbeat · Campaign feed · Override"]
USBMOD["UsbExportModule\nOffline package builder"]
ANALYTICS["AnalyticsModule\nViews · CTR · System / Campaign reports"]
end
subgraph PLATFORM ["Platform Services"]
PAYMENTS["PaymentModule\nCheckout · Webhooks · Multi-gateway"]
NOTIF["NotificationsModule\nIn-app · Email · Bull queue"]
LOGGING["LoggingModule\nActivity audit trail"]
AICHAT["AiChatModule\nClaude AI assistant"]
I18N["I18nModule\nLanguages · Translations"]
LOC["LocationsModule\nCities · Regions · Venues"]
BRAND["BrandingModule\nLogo · Theme"]
LANDING["LandingModule\nPublic site · Contact forms"]
end
end
subgraph INFRA ["🗄️ Infrastructure"]
direction LR
MYSQL[("MySQL 8\nPrisma ORM — 40 models")]
REDIS[("Redis 7\nBull queues · Cache")]
FILES[/"uploads/media/\nDocker named volume"/]
end
subgraph EXTERNAL ["🌐 External Services"]
direction LR
PAYEXT["Payment Gateways\nStripe · Razorpay"]
SMTP["SMTP\n(Ethereal in dev)"]
CLAUDE["Claude AI API\nAnthropic"]
end
BROWSER -->|"HTTPS REST · Bearer JWT"| NGINX
TVAPP -->|"Screen Token"| NGINX
NGINX --> API
API --> MYSQL
API --> REDIS
MEDIA --> FILES
PAYMENTS <-->|"Checkout + Webhook"| PAYEXT
AUTH -->|"Verification email"| SMTP
NOTIF -->|"Email alerts"| SMTP
AICHAT <-->|"Messages API"| CLAUDE
style CLIENTS fill:#e8eaf6,stroke:#3949ab
style NGINX_LAYER fill:#f3e5f5,stroke:#6a1b9a
style API fill:#e0f2f1,stroke:#00695c
style INFRA fill:#fff8e1,stroke:#f57f17
style EXTERNAL fill:#fce4ec,stroke:#880e4f
sequence-auth.md
sequenceDiagram
actor U as User (Browser)
participant FE as React Frontend
participant API as NestJS AuthModule
participant DB as MySQL
participant MAIL as SMTP Service
rect rgb(232, 245, 233)
Note over U,MAIL: REGISTRATION
U->>FE: Fill register form (email, password, role)
FE->>API: POST /api/auth/register
API->>DB: Check email uniqueness
DB-->>API: OK
API->>API: bcrypt.hash(password, 10)
API->>DB: INSERT User { email, hash, role, emailVerified: false }
API->>MAIL: Send verification email (token link)
API-->>FE: 201 { message: "Check your inbox" }
FE-->>U: Show verify notice
end
rect rgb(227, 242, 253)
Note over U,MAIL: EMAIL VERIFICATION
U->>FE: Click verification link
FE->>API: GET /api/auth/verify-email?token=xxx
API->>DB: Validate token → SET emailVerified=true
API-->>FE: 200 { message: "Email verified" }
FE-->>U: Redirect to login
end
rect rgb(255, 248, 225)
Note over U,MAIL: LOGIN
U->>FE: Enter email + password
FE->>API: POST /api/auth/login
API->>DB: Find user by email
DB-->>API: User record
API->>API: bcrypt.compare(password, hash)
alt Invalid credentials
API-->>FE: 401 Unauthorized
FE-->>U: Show error
else Valid credentials
API->>API: Sign accessToken (JWT 24h) + refreshToken (7d)
API-->>FE: 200 { accessToken, refreshToken, user }
FE->>FE: Store tokens, dispatch Redux auth.setUser
Note right of FE: ADMIN → /admin/dashboard\nADVERTISER → /advertiser/dashboard\nPUBLISHER → /publisher/dashboard
FE-->>U: Redirect by role
end
end
rect rgb(252, 228, 236)
Note over U,MAIL: AUTHENTICATED REQUESTS
U->>FE: Navigate to protected page
FE->>API: GET /api/* { Authorization: Bearer accessToken }
API->>API: JwtAuthGuard validates token
API->>API: RolesGuard checks user.role vs @Roles()
alt Token expired
API-->>FE: 401
FE->>API: POST /api/auth/refresh { refreshToken }
API-->>FE: New accessToken
FE->>API: Retry original request
else Role forbidden
API-->>FE: 403 Forbidden
FE-->>U: Redirect to /unauthorized
else Valid
API-->>FE: 200 ResponseHelper.success(data)
FE-->>U: Render page
end
end
rect rgb(237, 231, 246)
Note over U,MAIL: SCREEN LOGIN (TV App)
participant SCREEN as TV Screen App
SCREEN->>API: POST /api/auth/screen-login { activationCode }
API->>DB: Validate screen_codes WHERE code=? AND used=false
API->>DB: UPDATE Screen SET status=ACTIVE
API->>API: Sign screenToken (JWT, type: screen)
API-->>SCREEN: 200 { screenToken, screenConfig }
end
sequence-campaign.md
sequenceDiagram
actor ADV as Advertiser
actor ADM as Admin
participant FE as React Frontend
participant API as NestJS API
participant DB as MySQL
participant REDIS as Redis (Bull Queue)
participant STORE as File Storage
participant TV as TV Screen App
rect rgb(227, 242, 253)
Note over ADV,TV: STEP 1 — MEDIA UPLOAD
ADV->>FE: Upload creative (image/video)
FE->>API: POST /api/media/upload (multipart/form-data)
API->>STORE: Multer saves to uploads/media/uuid.ext
API->>DB: INSERT Media { status: PENDING, advertiserId }
API-->>FE: 201 { mediaId, status: PENDING }
FE-->>ADV: "Awaiting admin approval"
end
rect rgb(255, 248, 225)
Note over ADV,TV: STEP 2 — ADMIN APPROVAL
ADM->>FE: Open Media Approval panel
FE->>API: GET /api/media/pending
API->>DB: SELECT Media WHERE status=PENDING
API-->>FE: Pending media list
ADM->>FE: Click Approve
FE->>API: PATCH /api/media/:id { status: APPROVED }
API->>DB: UPDATE Media SET status=APPROVED
API->>DB: INSERT Notification for advertiser
API-->>FE: 200 OK
end
rect rgb(232, 245, 233)
Note over ADV,TV: STEP 3 — CAMPAIGN CREATION
ADV->>FE: Create new campaign
FE->>API: GET /api/screens (search/filter by city, venue)
API->>DB: SELECT Screens WHERE active=true + filters
API-->>FE: Screen list with rates
ADV->>FE: Select screens, set budget + schedule
FE->>API: POST /api/campaigns { mediaId, screenIds[], startDate, endDate, budget }
API->>DB: Check walletBalance >= estimatedCost
alt Insufficient balance
API-->>FE: 402 Insufficient balance
FE-->>ADV: Prompt to top up
else Balance OK
API->>DB: INSERT Campaign { status: SCHEDULED }
API->>DB: INSERT CampaignScreens[] (one row per screen)
API->>REDIS: scheduleJob("activateCampaign", { campaignId }, delay)
API-->>FE: 201 { campaignId, status: SCHEDULED }
end
end
rect rgb(252, 228, 236)
Note over ADV,TV: STEP 4 — ACTIVATION & CHARGING
REDIS->>API: Bull job fires at startDate
API->>DB: UPDATE Campaign SET status=ACTIVE
loop Every ad play (reported by TV App)
API->>DB: SELECT ScreenRate + walletBalance
alt Balance depleted
API->>DB: UPDATE Campaign SET status=PAUSED
API->>DB: INSERT Notification (low balance alert)
else OK
API->>DB: INSERT ChargingTransaction (DEBIT advertiser)
API->>DB: UPDATE walletBalance -= amount
API->>DB: INSERT ChargingTransaction (CREDIT publisher)
end
end
REDIS->>API: Bull job fires at endDate
API->>DB: UPDATE Campaign SET status=COMPLETED
end
rect rgb(237, 231, 246)
Note over ADV,TV: STEP 5 — TV DELIVERY
TV->>API: GET /api/tv/screens/:id/campaigns
API->>DB: SELECT active campaigns WHERE screenId AND schedule matches NOW()
API-->>TV: [{ mediaUrl, duration, order }]
loop Campaign rotation
TV->>STORE: GET /uploads/media/filename
TV->>TV: Display ad for durationSeconds
end
end
rect rgb(255, 243, 224)
Note over ADV,TV: STEP 6 — ANALYTICS
TV->>API: POST /api/tv/screens/:id/analytics/sync { plays[] }
API->>DB: INSERT Analytics records (bulk)
ADV->>FE: View Campaign Analytics
FE->>API: GET /api/analytics/campaigns/:id
API->>DB: Aggregate views, spend, reach by screen + date
API-->>FE: { totalViews, totalSpend, dailyTrend[], screenBreakdown[] }
FE-->>ADV: Charts and stats
end
sequence-screen.md
sequenceDiagram
actor PUB as Publisher
participant FE as React Frontend
participant API as NestJS API
participant DB as MySQL
participant STORE as File Storage
participant SCR as Screen Device (TV App)
rect rgb(232, 245, 233)
Note over PUB,SCR: STEP 1 — SCREEN REGISTRATION
PUB->>FE: Add Screen (name, city, venue, size)
FE->>API: POST /api/publisher/screens
API->>DB: INSERT Screen { status: PENDING_ACTIVATION }
API->>DB: INSERT screen_codes { code: random 6-char }
API-->>FE: 201 { screenId, activationCode: "ABC123" }
FE-->>PUB: Show activation code + QR
end
rect rgb(227, 242, 253)
Note over PUB,SCR: STEP 2 — TV APP ACTIVATION
SCR->>API: POST /api/auth/screen-login { activationCode }
API->>DB: Validate screen_codes WHERE code=? AND used=false
API->>DB: UPDATE screen_codes SET used=true
API->>DB: UPDATE Screen SET status=ACTIVE
API->>API: Sign screenToken = jwt.sign({ sub: screenId, type: screen })
API-->>SCR: 200 { screenToken, config }
SCR->>SCR: Persist screenToken
end
rect rgb(255, 248, 225)
Note over PUB,SCR: STEP 3 — RATE SETUP
PUB->>FE: Set pricing for screen
FE->>API: POST /api/charging/rates { screenId, pricePerSlot, slotDurationSeconds }
API->>DB: INSERT ScreenRate { isActive: true }
API-->>FE: 201 rate saved
FE-->>PUB: "Screen now visible to advertisers with pricing"
end
rect rgb(252, 228, 236)
Note over PUB,SCR: STEP 4 — HEARTBEAT LOOP
loop Every 60 seconds
SCR->>API: POST /api/tv/screens/:id/heartbeat { status, deviceInfo }
API->>DB: UPSERT HeartbeatLog { timestamp: NOW() }
API->>DB: UPDATE Screen SET lastSeenAt=NOW()
API->>DB: SELECT pending commands for screen
API-->>SCR: 200 { commands: [], configUpdate: null }
SCR->>SCR: Process returned commands
end
end
rect rgb(237, 231, 246)
Note over PUB,SCR: STEP 5 — CAMPAIGN FETCH & PLAYBACK
SCR->>API: GET /api/tv/screens/:id/campaigns
API->>DB: SELECT active campaigns WHERE screenId AND NOW() in schedule
API-->>SCR: [{ campaignId, mediaUrl, type, durationSeconds }]
loop Campaign rotation
SCR->>SCR: Check local media cache
alt Not cached
SCR->>API: GET /uploads/media/filename
API->>STORE: Read file
STORE-->>API: Binary data
API-->>SCR: Media file
SCR->>SCR: Cache locally
end
SCR->>SCR: Display ad for durationSeconds
end
end
rect rgb(255, 243, 224)
Note over PUB,SCR: STEP 6 — ANALYTICS SYNC
loop Every 5 minutes
SCR->>API: POST /api/tv/screens/:id/analytics/sync { plays[] }
API->>DB: INSERT Analytics[] bulk
API->>DB: UPDATE Campaign totalViews
API-->>SCR: 200 { synced: N }
SCR->>SCR: Clear local play log
end
end
sequence-payments.md
sequenceDiagram
actor ADV as Advertiser
actor ADM as Admin
participant FE as React Frontend
participant API as NestJS PaymentModule
participant GW as Payment Gateway (Stripe/Razorpay)
participant DB as MySQL
participant NOTIF as NotificationsModule
actor PUB as Publisher
rect rgb(227, 242, 253)
Note over ADV,PUB: STEP 1 — CHOOSE GATEWAY
ADV->>FE: Open "Add Funds" → select amount
FE->>API: GET /api/payments/gateways
API->>DB: SELECT PaymentGateway WHERE isActive=true
API-->>FE: Active gateways list
ADV->>FE: Select gateway + confirm amount
end
rect rgb(232, 245, 233)
Note over ADV,PUB: STEP 2 — CHECKOUT SESSION
FE->>API: POST /api/payments/create-checkout-session { amount, gatewayId }
API->>DB: INSERT PaymentTransaction { status: PENDING, reference: uuid }
API->>GW: Create payment session
GW-->>API: { sessionId, checkoutUrl }
API-->>FE: { checkoutUrl }
FE-->>ADV: Redirect to gateway checkout
end
rect rgb(255, 248, 225)
Note over ADV,PUB: STEP 3 — USER PAYS ON GATEWAY
ADV->>GW: Complete payment (card + 3DS)
GW-->>ADV: Success page
ADV->>FE: Redirect to /payment/success
end
rect rgb(252, 228, 236)
Note over ADV,PUB: STEP 4 — WEBHOOK (server-to-server)
GW->>API: POST /api/payments/webhook/:gateway (signed)
API->>API: Verify HMAC-SHA256 signature
alt Signature invalid
API-->>GW: 400 Bad Request
else Already processed (idempotency)
API-->>GW: 200 OK (no-op)
else New payment
API->>DB: UPDATE PaymentTransaction SET status=COMPLETED
API->>DB: UPDATE User SET walletBalance += amount
API->>DB: INSERT Transaction { type: CREDIT }
API->>NOTIF: Notify advertiser
NOTIF-->>ADV: "₹X added to your balance"
API-->>GW: 200 OK
end
end
rect rgb(237, 231, 246)
Note over ADV,PUB: STEP 5 — CAMPAIGN CHARGING (per play)
loop Per ad play event
API->>DB: SELECT ScreenRate + walletBalance
alt Balance < pricePerSlot
API->>DB: UPDATE Campaign SET status=PAUSED
NOTIF-->>ADV: "Campaign paused: low balance"
else OK
API->>DB: INSERT ChargingTransaction (DEBIT advertiser)
API->>DB: UPDATE walletBalance -= pricePerSlot
API->>DB: INSERT ChargingTransaction (CREDIT publisher)
end
end
end
rect rgb(255, 243, 224)
Note over ADV,PUB: STEP 6 — PUBLISHER PAYOUT
PUB->>FE: Request payout (amount)
FE->>API: POST /api/publisher/payout-request
API->>DB: INSERT PayoutRequest { status: PENDING }
ADM->>FE: Review payout requests
ADM->>FE: Approve payout
FE->>API: PATCH /api/publisher/payout-requests/:id { status: APPROVED }
API->>DB: UPDATE PayoutRequest SET status=APPROVED
API->>DB: UPDATE Store earnings -= amount
NOTIF-->>PUB: "Payout of ₹X approved"
end