Digital Advertising Platform for Screen Networks

AdSpot connects advertisers with digital screen publishers. Create targeted campaigns, manage screen networks, process payments, and track real-time analytics — all from a single platform.

NestJS React 18 TypeScript Prisma ORM MySQL Socket.io Redis Ant Design Tailwind CSS

Introduction

AdSpot is a full-stack, multi-tenant digital out-of-home (DOOH) advertising platform. It provides a marketplace where advertisers can create and manage campaigns displayed on digital screens owned by publishers, while admins oversee the entire ecosystem.

What is DOOH Advertising? Digital Out-of-Home advertising uses digital screens in public spaces (malls, restaurants, gyms, transit hubs) to display targeted ads. AdSpot automates the entire workflow from campaign creation to billing.

Key Highlights

🎯

Targeted Advertising

Target campaigns by city, region, venue type, and time slots for maximum impact.

💰

Automated Billing

Per-view charging with dynamic rate multipliers, daily budgets, and real-time spend tracking.

📡

Real-time Monitoring

Socket.io-powered live screen heartbeats, campaign status updates, and instant analytics.

🌍

Multi-language Support

Full i18n with RTL support, database-backed translations, and admin translation management.

Architecture

AdSpot follows a modular monolith architecture with clear separation between frontend and backend, connected via RESTful APIs and WebSocket channels.

┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ React Frontend │ │ NestJS Backend │ │ Database │ │ │ │ │ │ │ │ - React 18 + Vite │────►│ - REST API │────►│ - MySQL 8.0 │ │ - Redux Toolkit │ │ - WebSocket │ │ - Prisma ORM │ │ - Ant Design │◄────│ - JWT Auth │ │ - Redis Cache │ │ - Tailwind CSS │ │ - File Upload │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ │ ┌───────────┬───────────┐ │ │ ┌────┴────┐ ┌────┴────┐ ┌────┴─────┐ │ Stripe │ │ PayPal │ │ Razorpay │ └─────────┘ └─────────┘ └──────────┘

Tech Stack

Frontend

  • React 18 + TypeScript
  • Vite (build tool & HMR)
  • Ant Design (UI components)
  • Tailwind CSS (utility styling)
  • Redux Toolkit (state management)
  • React Router v7 (routing)
  • Socket.io Client (real-time)
  • Recharts (data visualization)

Backend

  • NestJS v11 + Express
  • TypeScript (strict mode)
  • Prisma ORM (type-safe queries)
  • MySQL 8.0 (primary database)
  • Redis (optional caching)
  • Passport + JWT (auth)
  • Socket.io (WebSockets)
  • Multer (file uploads)

DevOps & Tools

  • Docker + Docker Compose
  • Vercel (serverless deploy)
  • Prisma Migrations
  • ESLint + TypeScript checks
  • Swagger / OpenAPI docs
  • Nodemailer (SMTP email)
  • Supabase Storage (optional)
  • bcrypt (password hashing)
  • Jest + Supertest (E2E testing)
  • React Testing Library (unit tests)
  • Service Worker (offline support)

Features

📋

Campaign Management

Create, schedule, and track advertising campaigns with timeslot scheduling, location targeting, and budget controls.

💻

Screen Network

Register digital screens with code-based onboarding, heartbeat monitoring, and real-time status tracking.

🎨

Media Library

Upload images and videos with admin approval workflow. Manage media assets across campaigns.

📈

Analytics Dashboard

Real-time view tracking, revenue metrics, screen performance, and campaign ROI analysis.

💳

Payment Processing

Integrated Stripe, PayPal, and Razorpay gateways with webhook handling, balance management, and transaction history.

💲

Dynamic Pricing

Per-view charging with location multipliers, peak hour pricing, and configurable rate management.

👥

Multi-role Access

Dedicated dashboards for Admin, Advertiser, and Publisher roles with granular permission controls.

🌐

Internationalization

Database-driven i18n with RTL support, bulk translation management, and import/export capabilities.

Early Bird Program

Time-limited prelaunch offers with reduced fees, higher revenue shares, and trial credits for early adopters.

📝

Landing Page CMS

Admin-editable landing page with hero slides, benefits, features, testimonials, and how-it-works sections.

📍

Location Targeting

Configurable cities, regions, and venue types for precise geographic campaign targeting.

📜

Activity Logging

Comprehensive audit trail with IP tracking, user agent logging, and configurable retention policies.

🎨

Dynamic Branding

Admin-controlled business name, logo, and email that propagate across the landing page, dashboards, and notifications. Users can upload personal profile avatars.

💲

Multi-currency Support

Admin sets a platform-wide primary currency. All monetary values display with the correct symbol and conversion in real time across Admin, Advertiser, and Publisher views.

🔌

Offline Ad Playback

Service Worker-based media caching ensures ads continue playing during network outages, with automatic sync when connectivity is restored.

🔒

Screen Concurrency Control

WebSocket-based single-session enforcement per screen prevents double-billing and unauthorized concurrent connections.

🧪

Platform Fee & Tax System

Granular fee and tax calculation with per-transaction metadata, earnings breakdowns, and early-bird rate overrides.

Automated Testing

Jest unit tests and 18 E2E test suites covering financial calculations, WebSocket sessions, and API endpoints with HTML report generation.

How It Works

AdSpot operates as a three-sided marketplace connecting advertisers, publishers, and an admin platform.

For Advertisers

1
Sign Up

Create account & add funds

2
Upload Media

Images or videos

3
Create Campaign

Target screens & set budget

4
Go Live

Ads display on screens

5
Track Results

Views, spend & ROI

For Publishers

1
Register

Create publisher account

2
Add Screens

Register with code

3
Set Location

City, venue & coordinates

4
Display Ads

Screen shows campaigns

5
Earn Revenue

Request payouts

Campaign Lifecycle

1

Draft

Advertiser creates a campaign, selects media, targets screens by location/venue type, sets daily and total budgets, and configures timeslot schedules.

2

Active

Campaign goes live during scheduled times. Ads are displayed on assigned screens. Each view is tracked and charged against the campaign budget in real-time.

3

Monitoring

Advertisers track views, spend, and performance via the analytics dashboard. Daily budgets automatically reset at configured times.

4

Completion

Campaign ends when the date range expires or budget is exhausted. Final analytics and spend reports are available for review.

Use Cases

🏫

Retail Stores

Display promotional ads on in-store screens. Target specific locations, schedule ads during peak shopping hours, and track foot traffic impact.

🍔

Restaurants & Cafes

Digital menu boards and promotional displays. Schedule lunch specials during morning hours and dinner deals in the afternoon.

🏋

Gyms & Fitness

Target health-conscious audiences with fitness products, supplements, and wellness brand advertisements.

🚋

Transit Hubs

High-traffic locations like bus stations and airports. Maximize impressions with always-on campaigns targeting commuters.

🏪

Hotels & Hospitality

Lobby and common area displays for local attractions, restaurant recommendations, and event promotions.

🏬

Office Buildings

Corporate lobby displays for B2B advertising, tenant announcements, and professional service promotions.

🚮

Shopping Malls

Directory screens and promotional displays across multiple zones. Target by floor, wing, or venue type within the mall.

🏥

Healthcare

Waiting room displays in hospitals and clinics. Promote health services, pharmaceuticals, and wellness programs.

User Roles & Permissions

ADMIN Administrator

  • Full system access and configuration
  • User management (create, edit, deactivate, adjust balances)
  • Media content approval and rejection
  • Screen and campaign oversight
  • Payment gateway configuration (Stripe/PayPal/Razorpay)
  • Location management (cities, regions, venue types)
  • Language and translation management
  • Landing page CMS editing
  • Activity log review and audit
  • Early bird program configuration
  • Rate and charging management
  • Publisher payout approval
  • Platform-wide branding — business name, logo, primary currency
  • Upload profile avatar

ADVERTISER Advertiser

  • Create and manage advertising campaigns via full Campaign Workspace
  • Upload media (images/videos) for approval
  • Configure timeslot scheduling and location targeting
  • Set daily and total budget limits
  • Add funds via Stripe, PayPal, or Razorpay
  • View campaign analytics and ROI metrics
  • Track spending and transaction history
  • Monitor real-time campaign performance (spend, cost-per-day, budget %)
  • Upload profile avatar

PUBLISHER Publisher

  • Register and manage digital screens
  • Set screen location, venue type, and coordinates using admin-defined city/region lists
  • Monitor screen heartbeat and online status
  • View earnings with full Gross / Net breakdown
  • Request payouts (bank transfer)
  • Track screen occupancy and view metrics
  • Generate and manage screen access codes
  • Upload profile avatar

Campaign Management

Campaigns are the core of AdSpot. Advertisers create campaigns to display their media content on targeted digital screens. The create and edit flows are full-page Campaign Workspace views that include live Campaign Insights — showing real-time spend, remaining budget, cost per day, and budget usage — rather than simple modals.

Campaign Properties

PropertyDescription
Name & DescriptionCampaign title and details
MediaLinked approved media asset (image or video)
Date RangeStart and end dates for the campaign
Total BudgetMaximum total spend for the campaign
Daily BudgetMaximum daily spend (resets automatically)
Target ScreensSelected screens filtered by location and venue type
Schedule TypeALWAYS_ON or SCHEDULED with timeslot rules
TimezoneTimezone for schedule calculations
StatusDRAFTACTIVEPAUSEDCOMPLETED
Spent AmountReal-time total charged against the campaign so far
Cost Per DayCalculated daily spend rate based on recent charging history

Ad Scheduling (Time-Slot)

Campaigns can be set to Always On (run at all times when active) or a Custom Schedule where ads only display during specific days and hours. This lets advertisers target peak engagement windows — for example, a breakfast brand running ads Mon–Fri 7am–10am only.

FieldDescription
scheduleEnabledfalse = Always On; true = Custom Schedule active
scheduleData.timezoneIANA timezone string (e.g. America/New_York). Schedule rules are evaluated in this timezone.
scheduleData.rules[]Array of rule objects: { days, start, end }
rule.daysArray of day numbers: 0 = Sunday … 6 = Saturday
rule.start / rule.end24-hour time strings: "09:00""17:00"

Example schedule payload (Mon–Fri, 9am–5pm Eastern):

{
  "scheduleEnabled": true,
  "scheduleData": {
    "timezone": "America/New_York",
    "rules": [
      { "days": [1, 2, 3, 4, 5], "start": "09:00", "end": "17:00" }
    ]
  }
}
Intersection with Screen Availability A campaign schedule alone is not sufficient — the screen must also be open during the same window. An ad only plays when both the campaign schedule and the screen's availability window overlap. See the Time-Slot Scheduling section for the full two-gate model.

Campaign Lifecycle Automation

Two automated cron jobs manage campaign state without manual intervention:

  • Auto-Complete — Runs every minute; marks any ACTIVE campaign whose end_date has passed as COMPLETED.
  • Daily Spend Reset — Runs at midnight UTC; resets the daily_spent counter for all campaigns, restoring daily budget headroom.
Budget Protection Campaigns automatically pause when daily or total budgets are exhausted, preventing overspend. The midnight cron restores daily budget each day at 00:00 UTC.

Screen Management

Screens are digital displays registered by publishers that show advertising campaigns to viewers.

Screen Registration

Publishers register screens using a code-based system:

1

Generate Code

Publisher creates a new screen entry and receives a unique registration code.

2

Enter Code on Device

The physical screen device uses the code to authenticate and link to the publisher's account.

3

Configure Details

Set resolution, orientation, venue type, city/region, and GPS coordinates.

Screen Properties

PropertyDescription
ResolutionWidth and height in pixels
Orientationlandscape or portrait
Venue TypeRETAIL, RESTAURANT, CAFE, GYM, TRANSIT, OFFICE, HOSPITAL, MALL, HOTEL, OTHER
LocationCity, region, venue name, latitude/longitude
Statusactive, inactive, maintenance
Access CodeAuthentication code for screen device (regeneratable)

Heartbeat System

Active screens send periodic heartbeat signals to the server over the /screens WebSocket namespace, updating their last_active timestamp. This allows the platform to track screen online/offline status in real-time and alert publishers to connectivity issues. Each heartbeat also triggers a charging calculation for active campaigns.

Concurrency Control

Only one WebSocket session is permitted per screen at any time. Duplicate connection attempts are rejected with a screen:already_in_use event. Screens that stop sending heartbeats are automatically disconnected after 90 seconds, freeing the slot for re-authentication. See the Screen Concurrency section for full details.

Availability Schedule

Publishers can define operating hours for each screen — the hours during which the screen accepts and displays ad campaigns. Outside these hours, the screen returns an empty playlist regardless of what campaigns are assigned to it.

The availability schedule follows the same structure as campaign time-slot rules:

FieldDescription
availabilityEnabledfalse = Always available (default); true = Custom hours enforced
availabilitySchedule.timezoneIANA timezone string for evaluating the rules (e.g. Asia/Kolkata)
availabilitySchedule.rules[]Same { days, start, end } rule objects as campaign schedules

Example — a coffee shop screen open every day 8am–10pm IST:

PUT /api/screens/:id/availability
{
  "availabilityEnabled": true,
  "availabilitySchedule": {
    "timezone": "Asia/Kolkata",
    "rules": [
      { "days": [0,1,2,3,4,5,6], "start": "08:00", "end": "22:00" }
    ]
  }
}
Gate 1 — Hard Cutoff Screen availability is checked first before any campaign schedule. If the screen is outside its operating hours, the entire playlist returns empty — no campaign (not even an "Always On" one) will play. This ensures publishers have full control over when their screens display content.

Media Library

The media library stores all advertising content (images and videos) uploaded by advertisers.

Approval Workflow

1
Upload

Advertiser uploads media

2
Pending

Awaits admin review

3
Approved

Ready for campaigns

  • Supported formats: JPEG, PNG, GIF, MP4, AVI
  • Maximum file size: 10MB (configurable)
  • Admins can approve or reject media with reasons
  • Only approved media can be used in campaigns
  • Users can only view and manage their own media

Analytics & Reporting

AdSpot provides comprehensive analytics for all user roles.

Tracked Metrics

Views

Total views, view duration, views per screen, and views per campaign with time-series data.

Revenue

Revenue per view, total revenue, daily revenue, and revenue breakdown by screen/campaign.

Performance

Campaign ROI, screen occupancy rates, trend analysis, and comparative performance metrics.

Time Periods

Analytics can be filtered by predefined periods (7d, 30d, 90d) or custom date ranges. Each role sees metrics relevant to their perspective:

  • Admin: System-wide analytics, total revenue, user growth
  • Advertiser: Campaign performance, spend tracking, ROI
  • Publisher: Earnings, screen performance, occupancy metrics

Payment System

AdSpot integrates with Stripe, PayPal, and Razorpay for secure payment processing.

Payment Flow

1

Checkout Session

Advertiser initiates a payment. The backend creates a checkout session with the selected gateway (Stripe, PayPal, or Razorpay).

2

Payment Processing

User completes payment on the gateway's hosted page. Supports multiple currencies (default: USD).

3

Webhook Confirmation

Payment gateway sends a webhook to confirm the transaction. Raw body verification ensures security.

4

Balance Update

User's account balance is credited. Transaction is recorded in the payment history.

Webhook Security Stripe and Razorpay webhooks use signature verification. The backend processes webhook payloads before JSON parsing to preserve the original payload for signature validation.

Charging & Rate Management

AdSpot uses a per-view charging model with dynamic rate calculation.

Rate Calculation

Final Rate = Base Rate × Location Multiplier × Time Multiplier

Example:
Base Rate:           $0.05 per view
Location Multiplier: 1.5x  (mall location)
Time Multiplier:     2.0x  (peak hours 9AM-5PM)
────────────────────────────────
Final Rate:          $0.15 per view

Charging Features

  • 10-second billing intervals — Charges calculated per view period
  • Peak hour pricing — Configurable peak hours (default 9AM-5PM) with time multiplier
  • Location-based pricing — Different rates for different venue types
  • Budget validation — Prevents charging when daily/total budgets are exhausted
  • Platform fee & tax — Automatically deducted per transaction; configurable per-role rates
  • Audit trail — Every charge is recorded with rate, fee, and tax details for full transparency
See Also For a full explanation of the fee and tax calculation model, publisher earnings breakdown, and rate configuration, see the Platform Fees & Tax section.

Early Bird Program

A time-limited prelaunch program offering special benefits to early adopters.

BenefitEarly BirdRegular
Platform Fee5%15%
Publisher Revenue Share85%70%
Trial Credit$25None
Setup FeeFreeStandard
Duration12 months-

Admins can configure the program end date, available slots, pricing, and benefit percentages via the admin dashboard.

Internationalization (i18n)

AdSpot supports multiple languages with full RTL (right-to-left) layout support.

Features

  • Database-backed translations — All translations stored in the database, not JSON files
  • Namespace organization — Translations grouped by feature area
  • Status tracking — Each translation marked as missing, translated, needs_review, or approved
  • Bulk operations — Import/export translations, bulk updates
  • RTL support — Automatic layout direction switching for Arabic, Hebrew, etc.
  • Completion stats — Track translation coverage per language
  • Locale file sync — Validate and sync with frontend locale files

Dynamic Branding

AdSpot can be white-labelled entirely from the Admin dashboard. All brand-related settings persist in the database and propagate instantly across every surface — no code changes or redeployment required.

Configurable Brand Elements

ElementSurfaces
Business NameLanding page hero & footer, dashboard headers, email subjects & footers, browser title
LogoLanding page navbar, admin / advertiser / publisher sidebars, email headers
Admin Contact EmailLanding page newsletter / contact section displayed below the subscription subtitle

Logo Upload

The Admin settings page includes a logo upload control wired to a dedicated endpoint. The uploaded file is stored and served as the platform logo. Changing the logo takes effect immediately for all logged-in users on the next page refresh.

Profile Avatars

All user roles — Admin, Advertiser, and Publisher — can upload a personal profile avatar. The avatar is stored server-side and persists across sessions. It appears in the dashboard navigation bar and profile pages.

Where to configure Go to Admin → Settings → Branding to update the business name, logo, and contact email. Individual users can change their avatar from Profile → Edit Avatar.

Multi-currency Support

The platform operates in a single admin-defined primary currency. All monetary amounts — campaign budgets, charges, earnings, transaction history — display with the correct currency symbol and are converted in real time when the currency changes.

How It Works

  • Primary currency is set once by the Admin in System Settings (e.g. USD, EUR, GBP, INR). This acts as the platform base currency.
  • Dynamic symbol display — every monetary field across Admin, Advertiser, and Publisher dashboards renders the configured currency symbol automatically.
  • Real-time conversion — when the primary currency is changed, all displayed values update immediately without requiring data migration.
  • Payment gateways (Stripe, PayPal, Razorpay) are initiated with the configured currency code, ensuring checkout sessions match the platform setting.
Changing currency mid-operation Existing ChargingTransaction and PaymentTransaction records store raw numeric amounts without an embedded currency code. Changing the primary currency will cause historical records to display in the new currency symbol. For consistency, set the primary currency before going live and avoid changing it after real transactions have occurred.

Where to Configure

Go to Admin → Settings → Currency to select the platform primary currency from the supported list.

Platform Fees & Tax System

AdSpot applies a transparent, multi-tier fee and tax structure to every charging transaction. Fees are calculated per view and recorded with granular metadata for full auditability.

Fee Calculation

Advertiser pays:     Full charged amount (e.g. $10.00)

Platform Fee:        chargeAmount × platformFeeRate
                     e.g. $10.00 × 15% = $1.50

Publisher Tax:       platformFee × publisherTaxRate
                     e.g. $1.50 × 18% = $0.27

Total Deduction:     platformFee + tax = $1.77

Publisher Earning:   chargeAmount − totalDeduction = $8.23

Fee Rates

SettingDefaultEarly Bird
Platform Fee15%5%
Publisher Tax (on fee)18%18%
Advertiser Tax18%18%
Publisher Revenue Share70%85%

Publisher Earnings Breakdown

The Publisher Dashboard displays a complete earnings breakdown for full financial transparency:

  • Gross Revenue — Total amount charged to advertisers for views on the screen
  • Platform Fee — AdSpot's share deducted from gross revenue
  • Tax on Fee — Tax applied to the platform fee portion
  • Net Earnings — Amount actually credited to the publisher (Gross − Platform Fee − Tax)
Metadata Tracking Every ChargingTransaction record stores the exact rates applied at the time of charge — base rate, location multiplier, time multiplier, platform fee rate, and tax rate — ensuring complete auditability even if rates change later.

Admin Configuration

All fee rates are configurable from the Admin dashboard under System Settings. Changes apply to future transactions only; existing transaction records retain their original rates.

Offline Ad Support & Sync

Screens continue playing ads even when internet connectivity is disrupted. A Service Worker intercepts media requests and serves cached content as a fallback, then automatically synchronizes when the network is restored.

How It Works

1

Media Caching (Online)

When a screen fetches ad media (/api/media/) successfully, the Service Worker stores the response in the ads-media-cache-v1 browser cache. Images and videos are cached on first load.

2

Network-First Fetch Strategy

Every subsequent media request first attempts a live network fetch to get the freshest content. On success, the cache is updated. This ensures screens always have up-to-date ad content when online.

3

Offline Fallback

If the network request fails (connectivity loss), the Service Worker serves the previously cached media directly. The ad loop continues uninterrupted using locally cached images and videos.

4

Video Streaming Support

Cached videos support HTTP range requests (byte-range / partial content, HTTP 206). The Service Worker reconstructs range responses from the full cached video, enabling smooth video playback offline.

5

Automatic Sync on Reconnect

When connectivity is restored, the screen automatically resumes live fetching, updates cached content with any new assets, and re-establishes its WebSocket heartbeat connection to resume normal billing.

Implementation Details

ComponentLocationRole
Service Workerpublic/sw.jsIntercepts media requests, manages cache
SW Registrationsrc/main.tsxRegisters SW on app load
Cache Nameads-media-cache-v1Versioned cache store for media assets
Intercepted Routes/api/media/*All ad media fetch requests
Graceful Degradation If a media asset is not yet cached and the network is unavailable, the Service Worker returns a 503 Service Unavailable response. The screen player handles this gracefully by skipping the unavailable asset and continuing with cached content.

Screen Concurrency Restriction

To prevent double-billing and unauthorized simultaneous sessions, AdSpot enforces a strict one-active-connection-per-screen rule via the WebSocket gateway.

How It Works

The ScreensGateway maintains an in-memory map of active screen connections. When a screen authenticates over WebSocket:

  • The gateway checks if another socket is already registered for the same screen ID.
  • If a duplicate is detected, the new connection is rejected immediately with a screen:already_in_use event.
  • Only one socket may actively stream heartbeats and trigger billing for any given screen at a time.
  • When a screen disconnects, its entry is removed from the map, allowing a new connection to authenticate.

WebSocket Events

EventDirectionDescription
screen:authenticateClient → ServerScreen sends access code to authenticate. Blocked if screen already has an active session.
screen:already_in_useServer → ClientEmitted to a rejected connection attempt when another session is active for the same screen.
screen:heartbeatClient → ServerPeriodic health signal. Triggers charging. Resets the 90-second timeout timer.
screen:disconnectedServer → ClientNotifies client of server-side disconnect (e.g. heartbeat timeout).

Heartbeat Timeout

Each connected screen has a 90-second inactivity timer. A global monitor runs every 30 seconds and disconnects any screen that has not sent a heartbeat within the window. This prevents "zombie" connections that could block legitimate sessions.

Billing Integrity Because charging is triggered only on heartbeat events and only one socket per screen can send heartbeats, the concurrency restriction directly prevents double-billing — a screen can never be charged twice for the same view interval.

WebSocket Gateway Config

Namespace:      /screens
Ping Interval:  25 000 ms   (server → client keep-alive ping)
Ping Timeout:   60 000 ms   (client must respond within 60 s)
HB Timeout:     90 000 ms   (server disconnects if no heartbeat in 90 s)
HB Monitor:     every 30 s  (global check across all connected screens)

Time-Slot Based Ad Scheduling

AdSpot enforces a two-gate intersection model for determining when an ad plays on a screen. Both gates must be open simultaneously for any ad to be displayed.

📅

Gate 1 — Screen Availability

Publisher-defined operating hours. The screen's hard cutoff — if the screen is closed, nothing plays regardless of campaign settings.

🕑

Gate 2 — Campaign Schedule

Advertiser-defined active hours per campaign. Controls when a specific campaign is eligible to display on any screen.

Effective Display Window = Screen Availability ∩ Campaign Schedule
An ad only plays during the intersection of both windows. If a screen is open 8am–10pm and a campaign runs 9am–5pm weekdays, ads play Mon–Fri 9am–5pm only. Neither side alone determines the result.

Worked Example

Suppose Screen 1 is a coffee shop display with operating hours 12pm–1pm every day (lunch rush only). Three campaigns are assigned:

CampaignSchedulePlays on Screen 1?
Campaign A Always On ✓ Yes — campaign has no restriction; screen is open 12pm–1pm → ad plays 12pm–1pm
Campaign B Mon–Fri 10am–2pm ✓ Yes (weekdays) — intersection of 10am–2pm ∩ 12pm–1pm = 12pm–1pm on weekdays
Campaign C Mon–Fri 2pm–6pm ✗ No — campaign window (2pm–6pm) does not overlap with screen window (12pm–1pm)

Evaluation Logic

When a screen requests its active playlist (GET /api/campaigns/screen/:id), the backend applies the two gates in order:

1

Screen Gate

Is availabilityEnabled true? If yes, is the current time within any availability rule for this screen? If the screen is outside its operating hours → return an empty playlist [] immediately. No campaign is evaluated.

2

Campaign Gate

For each ACTIVE campaign assigned to the screen: is scheduleEnabled true? If yes, is the current time within one of its schedule rules? Campaigns outside their active window are filtered out. Always-On campaigns (scheduleEnabled: false) always pass this gate.

3

Playlist Returned

The campaigns that passed both gates form the active playlist delivered to the screen. Budget enforcement and charging run on top of this filtered set.

Timezone Handling

All schedule rules are evaluated in the timezone stored with the rule, not the server's local time. The backend uses Intl.DateTimeFormat.formatToParts() with the stored IANA timezone to extract the local day-of-week and HH:mm for comparison — no external packages required.

// Pseudocode — same logic for both campaign schedule and screen availability
function isOpen(enabled, schedule, now = new Date()) {
  if (!enabled) return true;                 // no restriction
  const { rules, timezone } = schedule;
  const { dayOfWeek, HH, mm } = extractParts(now, timezone);  // Intl API
  const currentTime = `${HH}:${mm}`;
  return rules.some(r =>
    r.days.includes(dayOfWeek) &&
    currentTime >= r.start &&
    currentTime <= r.end
  );
}
Empty Rules Semantics For screen availability: if availabilityEnabled = true but no rules are defined yet (publisher hasn't configured hours), the screen is treated as open — avoiding unintentional blackouts during setup.

For campaign schedule: if scheduleEnabled = true but no rules are defined, the campaign is treated as inactive (misconfigured) — preventing an accidental "always show" state when the advertiser intended to restrict it.

Data Model

ModelFieldTypeDefaultDescription
CampaignscheduleEnabledBooleanfalseActivates custom schedule mode
CampaignscheduleDataJSONnull{ timezone, rules[] }
ScreenavailabilityEnabledBooleanfalseActivates operating hours mode
ScreenavailabilityScheduleJSONnull{ timezone, rules[] }

Both fields share the same JSON rule shape:

{
  "timezone": "America/Chicago",       // IANA timezone string
  "rules": [
    {
      "days": [1, 2, 3, 4, 5],        // 0=Sun, 1=Mon ... 6=Sat
      "start": "09:00",               // 24-hour HH:mm
      "end": "17:00"
    },
    {
      "days": [6],                    // Saturday only
      "start": "10:00",
      "end": "14:00"
    }
  ]
}

API Reference

EndpointRoleDescription
PUT /api/campaigns/:id/scheduleAdvertiserSet or update campaign schedule (scheduleEnabled + scheduleData)
GET /api/campaigns/:id/schedule/statusAdvertiserCheck if campaign is currently within its schedule window
PUT /api/screens/:id/availabilityPublisherSet or update screen operating hours (availabilityEnabled + availabilitySchedule)
GET /api/campaigns/screen/:idScreenFetch active playlist — applies both gates before returning results

Frontend UI — ScheduleBuilder Component

Both the campaign form (Advertiser) and the screen edit form (Publisher) use a shared <ScheduleBuilder> React component. The Admin campaign detail view shows a read-only version of the same component.

  • Toggle — "Always On" vs "Custom Schedule" radio selector
  • Timezone picker — Searchable dropdown of all IANA timezones via Intl.supportedValuesOf('timeZone')
  • Day buttons — M T W T F S S toggles; enable/disable per day
  • Time range — Start & end TimePicker (24h format) per rule slot
  • Overlap guard — Inline error if two slots on the same day overlap
  • Weekly summary — "X active hrs/week" calculated from all enabled rules
Overlap Validation The ScheduleBuilder validates time slots in real time. If two rules for the same day overlap (e.g. 09:00–13:00 and 12:00–17:00), an inline error is shown and form submission is blocked. The same validation runs server-side in validateScheduleRulesOverlap() before any schedule is persisted.

Programmatic Buying (SSP/DSP Integration)

AdSpot includes a built-in Supply-Side Platform (SSP) that opens publisher screen inventory to external buyers — ad agencies, trading desks, and demand-side platforms (DSPs) such as Vistar Media, StackAdapt, and The Trade Desk. Buyers submit real-time bids for individual screens via an OpenRTB 2.5-compliant inbound API. On every screen heartbeat, AdSpot's mediation engine selects the highest-value winner between programmatic bids and existing direct campaigns.

OpenRTB 2.5 Inbound SSP Real-Time Mediation Confirmation Billing Pre-Funded Balance AI Chat
Inbound vs. Outbound Architecture AdSpot uses an inbound SSP model — buyers call AdSpot, not the other way around. Any buyer (agency, trading desk, SSP reseller) can integrate immediately by calling AdSpot's OpenRTB endpoints with their API key. No enterprise partnership with The Trade Desk is required to go live. Outbound calls to DSP bid endpoints are a future option once commercial agreements are in place.

Architecture Overview

Two actors and two request flows:

🏢

AdSpot Admin

Registers buyer accounts (DSP Connections), sets inbound API keys, monitors fill rate, win rate, avg CPM, and manages pre-funded buyer balances from the Admin → DSP Connections dashboard.

📡

Buyer / DSP / Agency

Calls POST /api/programmatic/bid to discover matching screen inventory, then POST /api/programmatic/submit to place a bid at a chosen price with a creative URL. No bid endpoint of their own is required.

📺

Publisher

Enables programmatic on each screen individually and sets a floor CPM. Only bids at or above the floor compete. Publishers see programmatic revenue alongside direct campaign earnings in Publisher Analytics.

⚖️

Mediation Engine

On each 30-second screen heartbeat, compares the best pending programmatic bid against the best direct campaign CPM. The higher value wins and its creative is delivered to the screen via WebSocket.

DSP Onboarding Flow

1

Admin Registers the Buyer

Via Admin → DSP Connections → Add DSP. Fields: Name, Inbound API Key (generated by AdSpot), Supported Venue Types, Bid Timeout (ms), Status (testing during integration, active for live billing), and Pre-funded Balance. No bid endpoint URL is needed for the inbound model.

2

Admin Shares Credentials with the Buyer

The buyer receives their Inbound API Key and two endpoint URLs: POST /api/programmatic/bid (discover inventory) and POST /api/programmatic/submit (place bid). AdSpot's OpenRTB 2.5 inventory spec is provided as documentation.

3

Publisher Enables Programmatic on Their Screen

Publisher → Screens → Edit → Programmatic Buying section. Toggle Enable Programmatic Buying ON and set a Floor CPM (e.g. $2.00 per 1,000 impressions). AdSpot only accepts bids at or above this floor.

4

Buyer Discovers Available Inventory

The buyer's system calls POST /api/programmatic/bid with an OpenRTB 2.5 bid request specifying venue types, city/region, and a minimum bid floor. AdSpot responds with matching screens and their details.

5

Buyer Submits a Bid

The buyer calls POST /api/programmatic/submit with their chosen screen ID, bid price, and creative URL (adMarkup — must be an https:// direct media URL). The bid is stored as pending for the next 30-second heartbeat window.

6

Mediation on Screen Heartbeat

On each screen heartbeat, AdSpot finds the highest pending bid within the last 30 seconds, compares it to the best direct campaign CPM (daily_price / 144 × 1000), and selects the winner. The winning creative is sent to the screen via screen:programmatic_ad WebSocket event.

7

Impression Confirmed & Billed

The screen renders the ad for displayDuration seconds, then emits screen:programmatic_ad_displayed back to the server. Only at this point is the DSP's balance debited and the publisher's earnings credited — preventing billing for failed renders.

Bid Discovery — POST /api/programmatic/bid

Authenticated via Authorization: Bearer <inbound-api-key> (or x-api-key header). Returns matching screens as an OpenRTB 2.5 BidResponse.

Request

POST /api/programmatic/bid
Authorization: Bearer <inbound-api-key>
Content-Type: application/json

{
  "id": "buyer-auction-id-abc123",      // buyer's own auction ID (UUID)
  "imp": [{
    "id": "imp-1",
    "bidfloor": 2.00,                   // minimum CPM the buyer will pay
    "bidfloorcur": "USD"
  }],
  "dooh": {
    "venuetype": ["transit", "retail"]  // optional: filter by venue
  },
  "site": {
    "geo": {
      "city": "Mumbai",                 // optional: filter by location
      "region": "Maharashtra"
    }
  }
}

Response

{
  "id": "buyer-auction-id-abc123",
  "seatbid": [{
    "bid": [
      {
        "id": "scr-xxxxxxxx",
        "price": 2.50,                  // screen's floor CPM
        "w": 1920, "h": 1080,
        "ext": {
          "screenId": "scr-xxxxxxxx",
          "screenName": "Mall Entrance — Ground Floor",
          "venuetype": "1.2",           // IAB DOOH venue code
          "location": "Mumbai, Maharashtra"
        }
      }
    ]
  }]
}

Bid Submission — POST /api/programmatic/submit

After reviewing the inventory response, the buyer submits a bid for a specific screen:

POST /api/programmatic/submit
Authorization: Bearer <inbound-api-key>

{
  "screenId": "scr-xxxxxxxx",
  "bidPrice": 3.50,                     // CPM in USD — must be ≥ screen's floor
  "adMarkup": "https://cdn.buyer.com/creative.mp4",  // https:// direct URL only
  "winNoticeUrl": "https://buyer.com/win?price=${AUCTION_PRICE}",
  "lossNoticeUrl": "https://buyer.com/loss?reason=${AUCTION_LOSS}",
  "currency": "USD"
}
Ad Markup Security adMarkup must be a direct https:// URL pointing to an image (JPEG, PNG, WebP) or video (MP4, WebM). http://, data URIs, and javascript: URLs are rejected. VAST XML and HTML/JS banners are out of scope for v1.

Mediation Engine

On each screen:heartbeat event (~30 seconds), the engine runs:

StepLogic
1. Check programmatic flagIf screen.programmaticEnabled = false, skip to direct campaign charging (existing path).
2. Find best pending bidQuery programmatic_bids WHERE status = 'pending' AND created_at > NOW() - 30s ORDER BY bidPrice DESC LIMIT 1.
3. Find best direct CPMQuery active CampaignScreen records: estimate CPM as (daily_price / 144) × 1000. Take the maximum.
4. CompareprogrammaticWins = bestBid && bestBid.bidPrice ≥ floorCpm && bestBid.bidPrice > bestDirectCpm
5a. Programmatic winsMark bid pending_display → emit screen:programmatic_ad to screen → await display confirmation → bill on confirm.
5b. Direct winsRun chargingService.processScreenCharging() (existing path). If a bid existed but lost, mark it lost and fire loss notice.

Bid Status Lifecycle

StatusMeaning
pendingBid submitted; eligible for the next heartbeat mediation window.
pending_displayBid won mediation; creative sent to screen; waiting for display confirmation.
wonScreen confirmed display. DSP balance debited; publisher earnings credited.
lostOutbid by a higher programmatic bid or a direct campaign with better CPM.
timeoutBid aged beyond the 30-second window without being selected.
errorDisplay confirmation not received within 15 seconds after sending the creative.
nobidNo bids were submitted for this screen in the current window.

Win & Loss Notices

AdSpot fires fire-and-forget HTTP callbacks to the buyer's registered URLs using OpenRTB macro substitution:

  • Win notice (nurl) — fired when screen:programmatic_ad_displayed is confirmed. ${AUCTION_PRICE} is replaced with the clearing price.
  • Loss notice (lurl) — fired when a bid loses mediation. ${AUCTION_LOSS} is replaced with an IAB loss reason code (e.g. 102 = lost to higher bid).
# Win notice example (called server-side, fire-and-forget)
GET https://buyer.com/win?price=3.50&screen=scr-xxxxxxxx

# Loss notice example
GET https://buyer.com/loss?reason=102&screen=scr-xxxxxxxx

Billing & Fee Split

Billing is triggered only after the screen emits screen:programmatic_ad_displayed. The gross amount is calculated as bidPrice (CPM) / 1000 per impression. The split mirrors the direct campaign model:

ComponentFormulaExample ($3.50 CPM bid)
Gross per impressionbidPrice / 1000$0.0035
Platform feegross × platformFeePercent$0.000525 (15%)
Tax on feeplatformFee × publisherTaxvaries by config
Publisher earninggross − fee − tax~$0.002975
DSP balance debitgross−$0.0035
Publisher balance creditpublisherEarning+$0.002975

All three operations (create ProgrammaticImpression, decrement DspConnection.balance, increment User.balance for publisher) run inside a single Prisma transaction.

Pre-funded Balance Required DSPs must maintain a positive balance in AdSpot to submit bids. The admin credits buyer accounts via Admin → DSP Connections → Manage Balance. If a DSP's balance is insufficient to cover a bid, the bid submission is rejected with 402 Insufficient Balance.

Database Models

DspConnection

FieldTypeDescription
idUUIDPrimary key
nameStringBuyer display name (e.g. "Vistar Media")
inboundApiKeyStringSecret key the buyer includes in every API call
endpointString?Future: outbound bid endpoint URL
statusDspStatusactive | inactive | testing
bidTimeoutMsIntMax ms to wait for bid response (default 150)
supportedVenueTypesJSONArray of venue types this buyer targets
balanceDecimalPre-funded USD balance; decremented on each won impression
enabledBooleanToggle to pause a buyer without deleting

ProgrammaticBid

FieldTypeDescription
auctionIdUUIDBuyer's own auction reference ID
dspIdUUIDFK → DspConnection
screenIdUUIDFK → Screen
bidPriceDecimal(10,4)Bid CPM in USD
adMarkupTextCreative URL (https:// only)
winNoticeUrlString?URL called on win with ${AUCTION_PRICE} macro
lossNoticeUrlString?URL called on loss with ${AUCTION_LOSS} macro
statusProgrammaticBidStatuspending → pending_display → won / lost / error / timeout
lossReasonCodeInt?IAB OpenRTB loss reason code

ProgrammaticImpression

FieldTypeDescription
screenIdUUIDScreen that displayed the ad
bidIdUUIDWinning ProgrammaticBid
publisherIdUUIDPublisher who owns the screen
dspIdUUIDBuyer that won the auction
grossAmountDecimalTotal charged to DSP
platformFeeDecimalAdSpot's cut
publisherEarningDecimalAmount credited to publisher
impressionAtDateTimeTimestamp of confirmed display

Screen Extension

Two new fields are added to the Screen model:

FieldDefaultDescription
programmaticEnabledfalsePublisher opt-in toggle. Must be true for the screen to participate in auctions.
programmaticFloorCpmnullMinimum acceptable CPM. Bids below this value are rejected at submission time.

Updated via: PATCH /api/screens/:id/programmatic (Publisher role required).

API Endpoints Reference

Admin Endpoints (JWT + ADMIN role)

GET /api/programmatic/dsps List all DSP connections
POST /api/programmatic/dsps Create a new DSP connection
PUT /api/programmatic/dsps/:id Update DSP connection details or status
DELETE /api/programmatic/dsps/:id Remove a DSP connection
PATCH /api/programmatic/dsps/:id/balance Credit or debit a DSP's pre-funded balance
GET /api/programmatic/auctions Paginated auction history (filter by screenId, dspId, status)
GET /api/programmatic/impressions Paginated impression log with fee breakdown
GET /api/programmatic/stats Aggregate stats: fill rate, win rate, avg CPM, total revenue

Buyer Endpoints (x-api-key / Bearer token)

POST /api/programmatic/bid Discover available screen inventory (OpenRTB 2.5 BidRequest)
POST /api/programmatic/submit Submit a bid for a specific screen

Publisher Endpoints (JWT + PUBLISHER role)

PATCH /api/screens/:id/programmatic Enable/disable programmatic and set floor CPM for a screen
GET /api/publisher/programmatic-analytics Programmatic impression stats with daily breakdown (startDate, endDate params)

Venue Type → IAB DOOH Code Map

AdSpot Venue TypeIAB DOOH Code
RETAIL1.1
MALL1.2
RESTAURANT2.1
CAFE2.2
GYM3.1
TRANSIT4.1
OFFICE5.1
HOSPITAL6.1
HOTEL7.1
OTHER0

Frontend Surfaces

🛠️

Admin → DSP Connections

Full CRUD for buyer accounts. Stats row (Total DSPs, Active, Fill Rate, Avg CPM). Tabs: DSP Connections · Auction History · Impressions. Balance credit/debit modal per DSP.

📡

Publisher → Add/Edit Screen

New Programmatic Buying card below Pricing: on/off toggle + conditional Floor CPM input. Settings saved via the dedicated PATCH /screens/:id/programmatic endpoint.

📊

Publisher → Analytics → Programmatic tab

Impressions, Revenue, Avg CPM stats cards + daily area chart of programmatic earnings. Loaded on demand when the tab is first selected.

📺

Screen Device (RemoteScreen)

Listens for screen:programmatic_ad WebSocket event. Renders external image/video fullscreen as a z-index overlay, interrupting the normal campaign rotation. Emits screen:programmatic_ad_displayed after displayDuration seconds to trigger billing.

AI Chat Assistant

AdSpot includes a built-in AI assistant (powered by Claude claude-haiku-4-5) accessible on every authenticated page as a floating chat widget, and on the landing page with predefined quick-start cards.

💬

Floating Widget

Visible on all authenticated pages (Admin, Advertiser, Publisher). Click the chat button (bottom-right) to open. Supports free-form conversation about the platform, screens, campaigns, and bids.

🏠

Landing Page Quick Start

Three predefined cards before free-form chat begins: About the Platform · Screen Availability · Ongoing Bids. Selecting a card sets the conversation topic and seeds the AI context with live database data.

The AI endpoint (POST /api/ai-chat/message) requires no authentication — making it accessible to unauthenticated landing page visitors. It fetches live data (active screens by venue and city, current DSP bid counts) when the topic is screens or bids, providing contextually accurate answers.

How does a DSP actually bid? Do they need their own bid endpoint?
No. In AdSpot's inbound SSP model, the DSP calls AdSpot's endpoint — they don't need to expose their own. The buyer calls POST /api/programmatic/bid to discover inventory and POST /api/programmatic/submit to place a bid. AdSpot does all the auction logic server-side. This means any buyer — even a small ad agency with no bidding infrastructure — can integrate in hours rather than months.
What happens if the screen fails to render the programmatic ad?
The bid is marked error and the DSP is not charged. Billing only happens after the screen emits screen:programmatic_ad_displayed. If no confirmation is received within 15 seconds, the server auto-expires the pending impression and the DSP retains their balance for that slot.
Can programmatic ads run alongside direct campaigns on the same screen?
Yes — that is exactly the mediation model. Every 30 seconds, AdSpot compares the best programmatic bid CPM against the best direct campaign CPM for that screen. The higher value wins. If no programmatic bids exist or none beat the direct CPM, the direct campaign runs as normal through the existing charging pipeline. The publisher is always compensated at the highest available rate.
How is the DSP balance managed?
DSPs operate on a pre-funded model. The admin credits a buyer's balance via Admin → DSP Connections → Manage Balance (credit or debit operation). The balance is checked at bid submission time — if it would not cover the potential gross amount, the bid is rejected. The actual debit happens only when a win is confirmed by the screen, never speculatively.
What ad formats does the programmatic endpoint support?
v1 supports direct media URLs only — JPEG, PNG, and WebP images, plus MP4 and WebM videos. The adMarkup field must be an https:// URL pointing directly to the media file. VAST XML, HTML/JS banners, and MRAID are out of scope for the current release. DSPs must provide a CDN-hosted direct URL, not a wrapper.

USB Export — Offline Digital Signage

Publishers can export campaign content packages as ZIP files, load them onto USB drives, and play ads on Android TV displays without any internet connection. USB mode is an extension of the existing AdSpot Flutter TV app — no separate player is needed.

Use Cases Remote locations with no reliable internet, trade shows, small retail shops, government offices, and any venue where a persistent connection is unavailable or unnecessary.

How It Works

1
Publisher selects a screen and date range on the USB Export page, then runs a Cost Preview to see the billing breakdown before committing.
2
Publisher clicks Generate & Download ZIP. The server collects all eligible campaign media, transcodes video to MP4/H.264, builds a signed playlist.json manifest, and bundles everything into a downloadable archive.
3
Publisher copies the ZIP to a USB drive and loads it into the Android TV device running the AdSpot Flutter app. The app detects the USB, validates the manifest signature and expiry, then plays the offline schedule.
4
After physically deploying the content, the publisher clicks Confirm Deployment in the dashboard. This releases the held delivery fee to the publisher and notifies advertisers their content is live.

Two-Stage Billing

USB exports use a two-stage billing model to protect advertisers from paying for content that is never deployed to a screen.

StageAmountWhenTransaction Type
Booking Fee 25% of campaign cost Immediately on export generation USB_EXPORT_BOOKINGCOMPLETED
Delivery Fee 75% of campaign cost Held until publisher confirms deployment USB_EXPORT_DELIVERYPENDING

If the publisher does not confirm deployment within the grace period after the export expires, all PENDING delivery transactions are auto-refunded to advertisers by a daily scheduled job.

Cost Calculation

Cost is calculated per campaign, then summed for the export total:

── Per-campaign ──────────────────────────────────────────────────────────────
Base Cost      = usbBaseDailyRate × numberOfDays × locationMultiplier
               × earlyBirdMultiplier  (if advertiser qualifies; otherwise × 1)

Booking Fee    = Base Cost × usbBookingFeePercent / 100     (default: 25%)
Delivery Fee   = Base Cost − Booking Fee                    (default: 75%)

Platform Fee   = Base Cost × platformFeeRate
Tax            = Platform Fee × publisherTaxRate
Publisher Earning = Base Cost − Platform Fee − Tax

── Export totals ─────────────────────────────────────────────────────────────
Total Cost          = Σ Base Cost             (all included campaigns)
Total Booking Fee   = Σ Booking Fee           (all included campaigns)
Total Delivery Fee  = Σ Delivery Fee          (all included campaigns)
Total Publisher Earning = Σ Publisher Earning (all included campaigns)

Cost Parameters

ParameterDescriptionDefault
usbBaseDailyRateBase cost per campaign per day$0.50 (Admin-configurable)
locationMultiplierFrom the active ScreenRate record for the screenInherited from screen
numberOfDaysDuration of the export periodFrom startDate / endDate
earlyBirdMultiplierDiscount for early-bird advertisers (e.g. 0.5 = 50% off)usbEarlyBirdDiscount setting
platformFeeRatePlatform commission for the advertiser (early bird or standard)5% early bird / 15% standard
publisherTaxRateTax applied to the platform fee18%
usbBookingFeePercent% of base cost charged immediately at export time25

Advertiser Opt-In

Only campaigns with allowUsbExport = true are eligible for USB exports. Advertisers control this toggle per-campaign in their campaign settings. Default is off (safe by default).

  • When the toggle is off, the campaign is never included in any USB export regardless of targeting
  • When the toggle is on, the advertiser is shown the billing explanation: 25% charged at export time, 75% held until deployment is confirmed
  • Budget validation checks the full cost (booking + delivery) before including a campaign — if balance is insufficient, the campaign is excluded and the advertiser is notified

Admin Settings

SettingTypeDefaultDescription
usbBaseDailyRateDecimal0.50Base rate per campaign per day
usbMaxExportDaysInteger30Maximum allowed export duration in days
usbMaxPackageSizeMbInteger500Maximum ZIP size in MB
usbAllowedMediaFormatsString[]["mp4","jpg","png","gif"]Allowed media types for USB bundles
usbEarlyBirdDiscountDecimal0.5Cost multiplier for early-bird advertisers
usbBookingFeePercentInteger25% of total cost charged at export time
usbDeliveryGracePeriodDaysInteger7Days after expiry before unconfirmed delivery fee is auto-refunded

API Endpoints

MethodEndpointRoleDescription
POST/api/usb-export/:screenId/previewPublisherCost preview — returns per-campaign breakdown before any charges
POST/api/usb-export/:screenIdPublisherGenerate & download ZIP; charges booking fee, holds delivery fee
GET/api/usb-export/historyPublisher/AdminList past exports with pagination; filter by screen, date, status
GET/api/usb-export/:exportId/downloadPublisher/AdminRe-download a previous export ZIP
POST/api/usb-export/:exportId/confirm-deploymentPublisher/AdminMark export as deployed; releases delivery fee to publisher
PATCH/api/usb-export/:exportId/expireAdminForce-expire an export
GET/api/usb-export/settingsAdminGet global USB settings
PUT/api/usb-export/settingsAdminUpdate global USB settings

Flutter Player App (USB Mode)

USB mode is an extension of the existing AdSpot Flutter TV app — no separate install is required. When a USB drive is inserted, the app detects it, validates the manifest, and switches to offline playback mode automatically.

  • Manifest validation — Checks HMAC-SHA256 signature, expiry date, and screen ID match before loading
  • Schedule engine — Reuses schedule_utils.dart from the existing app to evaluate campaign schedules against USB manifest data
  • Fallback content — If no valid USB content is found, the app falls back to the configured fallback media
  • Auto-revert — When the USB is removed, the app returns to online mode and reconnects via WebSocket
Testing on Android Phone You don't need an Android TV box to test USB mode. The APK runs on any Android phone (Android 7.0 / API 24+) — the app includes a standard LAUNCHER intent filter alongside the TV launcher, and the USB Host API (ACTION_MEDIA_MOUNTED, UsbManager) is identical on both form factors.

You'll need a USB-C to USB-A OTG adapter to connect the USB drive to the phone. Build the APK in debug mode (flutter build apk --debug), sideload it, insert the USB drive via the adapter, and USB detection works exactly as on TV. Lock the phone to landscape orientation for accurate layout testing. Note: the Android Emulator cannot simulate physical USB mount events — a real device is required.

Notification System

AdSpot delivers real-time in-app notifications to users via WebSockets (Socket.io) and persists them in the database so nothing is lost if the user is offline.

🔔

Real-Time Push

Notifications are pushed instantly to connected clients over a dedicated Socket.io namespace (/notifications). Each user joins their own private room so messages are never leaked across accounts.

💾

Persistent Store

Every notification is written to the database before the WebSocket push. Users who are offline at the time of delivery retrieve their full notification history via the REST API when they reconnect.

👤

Admin Broadcast

The notifyAdmins() helper queries all users with the ADMIN role and fans out an individual notification to each one — no bespoke broadcast loop required at the call site.

Architecture

Two NestJS providers collaborate to deliver every notification:

1

NotificationsService

Receives a CreateNotificationDto, persists the record via Prisma, then delegates to the gateway for live delivery. Also exposes findAll, markAsRead, markAllAsRead, delete, and the notifyAdmins broadcast helper.

2

NotificationsGateway

A Socket.io WebSocket gateway under the /notifications namespace. On connection it places each client in a private room (user_<userId>) using the userId query parameter or a subsequent joinNotificationRoom message. Calls sendNotificationToUser() to emit to that room.

WebSocket Connection

Connect from the frontend using the Socket.io client targeting the /notifications namespace:

import { io } from 'socket.io-client';

const socket = io('/notifications', {
  query: { userId: currentUser.id },   // auto-joins room on connect
});

// Or join manually after connection:
socket.emit('joinNotificationRoom', currentUser.id);

// Listen for incoming notifications:
socket.on('notification', (notification) => {
  // { id, userId, title, message, type, isRead, metadata, created_at }
  dispatch(addNotification(notification));
});

Data Model

FieldTypeDescription
idstring (cuid)Unique identifier
userIdstringOwner of the notification
titlestringShort heading shown in the notification bell
messagestringFull notification body
typestringFree-form category tag (e.g. campaign, payment, media)
isReadbooleanRead state; defaults to false
metadataJSONOptional structured payload (e.g. linked entity IDs)
created_atDateTimeCreation timestamp

REST Endpoints

All endpoints require a valid JWT (Authorization: Bearer <token>). The service scopes queries to the authenticated user's ID automatically.

GET/api/notificationsList all notifications for the current user (newest first)
PUT/api/notifications/read-allMark every unread notification as read
PUT/api/notifications/:id/readMark a single notification as read
DELETE/api/notifications/:idDelete a notification
Note: Notifications are created internally by other services (e.g. campaign status changes, payment confirmations, media approvals) — there is no public POST /api/notifications endpoint. The NotificationsService.create() and notifyAdmins() methods are called directly from peer services via NestJS dependency injection.

Testing

AdSpot ships with a comprehensive automated test suite covering both unit and end-to-end scenarios.

Unit Tests

Critical frontend components and API interactions are covered with Jest and React Testing Library. Tests are colocated with their components and focus on rendered output and user interaction behaviour.

E2E Test Suite

The backend E2E suite uses Jest with Supertest (HTTP) and socket.io-client (WebSocket), running against a fully bootstrapped NestJS application with mocked Prisma and email providers.

Spec FileCoverage Area
charging.e2e-spec.tsRate calculation, platform fee/tax, budget enforcement
screen-concurrency.e2e-spec.tsWebSocket single-session enforcement, duplicate rejection
early-bird.e2e-spec.tsEnrollment, settings, trial credit, stats
campaigns.e2e-spec.tsCampaign CRUD, status transitions, budget operations
screens.e2e-spec.tsScreen registration, status management
payments.e2e-spec.tsCheckout sessions, webhook handling
analytics.e2e-spec.tsView tracking, dashboard statistics
auth.e2e-spec.tsJWT auth, login/register flows
publisher.e2e-spec.tsEarnings, payouts, dashboard
media.e2e-spec.tsUpload, approval workflow
+ 8 moreUsers, locations, i18n, branding, health, logging, landing, TV app

Test Commands

# Run all E2E tests
npm run test:e2e

# Watch mode (re-run on file change)
npm run test:e2e:watch

# Generate coverage report
npm run test:e2e:coverage

# Generate HTML test report
npm run test:e2e:report
# Output: test/reports/e2e-test-report.html
Test Isolation Each test suite creates its own isolated NestJS application instance with freshly mocked Prisma and email providers. Mocks are cleared between tests to prevent state leaking across test cases.

API Overview

The AdSpot API follows RESTful conventions with a global /api prefix. All responses are wrapped in a standard format:

{
  "success": true,
  "message": "Operation completed successfully",
  "data": { ... }
}

Pagination

List endpoints return paginated results with metadata:

{
  "success": true,
  "data": [ ... ],
  "meta": {
    "total": 150,
    "page": 1,
    "limit": 20,
    "hasNext": true,
    "hasPrev": false
  }
}

Authentication

The API uses JWT (JSON Web Token) for authentication with access and refresh token support.

TokenExpiryUsage
Access Token24 hoursSent as Bearer token in the Authorization header
Refresh Token7 daysUsed to obtain a new access token without re-login

Auth Endpoints

POST/api/auth/registerRegister a new user account
POST/api/auth/loginLogin and receive tokens
POST/api/auth/refreshRefresh access token
GET/api/auth/profileGet current user profile
GET/api/auth/verify-email/:tokenVerify email address
Public Endpoints Some endpoints are marked as public and don't require authentication. These include the landing page content, public screen display, and health checks.

API Endpoints

Users

GET/api/usersList all users (Admin)
GET/api/users/:idGet user by ID
PATCH/api/users/:idUpdate user profile
PATCH/api/users/:id/balanceUpdate user balance (Admin)
DELETE/api/users/:idDelete user (Admin)

Campaigns

GET/api/campaignsList campaigns
POST/api/campaignsCreate new campaign
GET/api/campaigns/:idGet campaign details
PATCH/api/campaigns/:idUpdate campaign
PATCH/api/campaigns/:id/statusChange campaign status
PATCH/api/campaigns/:id/scheduleUpdate timeslot schedule
DELETE/api/campaigns/:idDelete campaign

Screens

GET/api/screensList screens
POST/api/screensRegister new screen
GET/api/screens/:idGet screen details
PATCH/api/screens/:idUpdate screen
POST/api/screens/ns/registerRegister screen by code
POST/api/screens/:id/heartbeatSend screen heartbeat

Media

GET/api/mediaList media assets
POST/api/media/uploadUpload media file
GET/api/media/:idGet media details
PATCH/api/media/:id/approveApprove media (Admin)
DELETE/api/media/:idDelete media

Analytics

GET/api/analytics/overviewDashboard overview stats
GET/api/analytics/campaigns/:idCampaign analytics
GET/api/analytics/screens/:idScreen analytics
POST/api/analytics/record-viewRecord a campaign view

Payments

POST/api/payments/checkoutCreate checkout session
POST/api/payments/webhook/stripeStripe webhook handler
POST/api/payments/webhook/paypalPayPal webhook handler
POST/api/payments/webhook/razorpayRazorpay webhook handler
GET/api/payments/transactionsTransaction history

Publisher

GET/api/publisher/dashboardPublisher dashboard data
GET/api/publisher/earningsEarnings overview
POST/api/publisher/payoutsRequest a payout
GET/api/publisher/analyticsPublisher analytics

Locations

GET/api/locations/citiesList cities
GET/api/locations/regionsList regions
GET/api/locations/venue-kindsList venue types

i18n

GET/api/i18n/languagesList available languages
POST/api/i18n/languagesCreate language (Admin)
GET/api/i18n/translationsGet translations
POST/api/i18n/translations/bulkBulk update translations

Notifications

GET/api/notificationsList all notifications for the current user
PUT/api/notifications/read-allMark all notifications as read
PUT/api/notifications/:id/readMark a single notification as read
DELETE/api/notifications/:idDelete a notification

System

GET/api/healthBasic health check
GET/api/health/databaseDatabase connectivity check
GET/api/health/fullComprehensive system health
GET/api/docsThis documentation page

Project Structure

adspot_project/ ├── api/ # Backend (NestJS) │ ├── controllers/ # Standalone controllers (health, docs) │ ├── modules/ # Feature modules │ │ ├── auth/ # JWT auth, guards, decorators │ │ ├── users/ # User CRUD & management │ │ ├── campaigns/ # Campaign lifecycle │ │ ├── screens/ # Screen registration & heartbeats │ │ ├── media/ # File upload & approval │ │ ├── analytics/ # View tracking & metrics │ │ ├── payments/ # Stripe, PayPal & Razorpay gateways │ │ ├── charging/ # Rate management & billing │ │ ├── publisher/ # Publisher earnings & payouts │ │ ├── i18n/ # Languages & translations │ │ ├── landing/ # Landing page CMS │ │ ├── locations/ # Cities, regions, venue types │ │ ├── early-bird/ # Early bird program │ │ └── logging/ # Activity audit logs │ ├── database/ # Database adapters & factory │ ├── prisma/ # Prisma service wrapper │ ├── seeders/ # Admin & data seeders │ ├── services/ # Email, logging, initialization │ ├── shared/ # DTOs, types, response helpers │ ├── filters/ # Exception filters │ └── middleware/ # Logging middleware ├── src/ # Frontend (React) │ ├── components/ # Reusable UI components │ │ ├── layout/ # Header, sidebar, navigation │ │ ├── landing/ # Landing page components │ │ └── ui/ # Shared UI elements │ ├── pages/ # Page components │ │ ├── admin/ # Admin dashboard pages │ │ ├── advertiser/ # Advertiser pages │ │ ├── publisher/ # Publisher pages │ │ ├── auth/ # Login, register, verify │ │ └── screen/ # Screen display pages │ ├── store/ # Redux store & slices │ ├── services/ # API service layer │ ├── hooks/ # Custom React hooks │ ├── contexts/ # React contexts (i18n, theme) │ └── lib/ # API client & utilities ├── prisma/ # Schema & migrations ├── public/ # Static assets & locales ├── Dockerfile # Multi-stage Docker build ├── docker-compose.yml # MySQL, Redis, App services └── vercel.json # Vercel serverless config

Environment Setup

Create a .env file in the project root with the following variables:

# Database
DATABASE_URL=mysql://username:password@localhost:3306/adspot_db
DATABASE_TYPE=mysql
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USERNAME=your_username
MYSQL_PASSWORD=your_password
MYSQL_DATABASE=adspot_db

# JWT Authentication
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d

# Server
PORT=3001
NODE_ENV=development

# URLs
API_BASE_URL=http://localhost:3001
FRONTEND_URL=http://localhost:5173

# File Upload
UPLOAD_MAX_SIZE=10485760
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,video/mp4,video/avi

# Payment Gateways
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
PAYPAL_CLIENT_ID=xxx
PAYPAL_CLIENT_SECRET=xxx
RAZORPAY_KEY_ID=rzp_test_xxx
RAZORPAY_KEY_SECRET=xxx
RAZORPAY_WEBHOOK_SECRET=xxx

# Email (SMTP)
SMTP_HOST=smtp.example.com
SMTP_USER=your_email@example.com
SMTP_PASS=your_password

# Supabase Storage (optional)
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=xxx

# Redis (optional)
REDIS_PASSWORD=redis_password

Installation

Prerequisites

  • Node.js 20+
  • npm or yarn
  • MySQL 8.0 (or Docker)
  • Redis (optional, for caching)

Quick Start

# Clone and install
git clone <repository-url>
cd adspot_project
npm install

# Setup database
npm run prisma:generate
npm run prisma:migrate:deploy

# Seed initial data
npm run seed:admin      # Admin user only
npm run seed:all        # All seed data

# Start development
npm run dev

Development

Commands

# Run both frontend and backend
npm run dev

# Run frontend only (Vite)
npm run client:dev

# Run backend only (NestJS)
npm run server:dev

# Open Prisma Studio (database GUI)
npm run prisma:studio

Development URLs

ServiceURL
Frontend (Vite)http://localhost:5173
Backend APIhttp://localhost:3001
Swagger Docshttp://localhost:3001/api/docs
Prisma Studiohttp://localhost:5555

Build

# Production build (frontend + backend)
npm run build:production

# Frontend only
npm run build:frontend

# Backend only
npm run build:backend

# Start production server
npm run start

Deployment

Docker Compose (Recommended)

# Start all services
docker-compose up -d

# View logs
docker-compose logs -f

# Stop services
docker-compose down

# Rebuild and start
docker-compose up -d --build

Docker Services

ServicePortDescription
app3002 (API), 5174 (Dev)Application server
mysql3307MySQL 8.0 database
redis6379Redis cache

Docker Standalone

# Build image
npm run docker:build

# Run container
npm run docker:run

Vercel (Serverless)

The project includes a vercel.json configuration for serverless deployment:

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel
Vercel Entry Point The serverless handler at api/index.ts bootstraps NestJS on each cold start and reuses the instance for subsequent requests.

Applying Updates

This section explains how to safely upgrade an existing AdSpot installation to a newer version — whether you are running a local development environment, a self-hosted server, or a Docker-based deployment.

Always back up first Before applying any update, take a database backup. Schema migrations cannot be automatically reversed.
mysqldump -u root -p adspot_db > adspot_backup_$(date +%F).sql

Standard Update (Self-hosted / PM2)

Follow these steps in order. Every step must complete successfully before moving to the next.

1

Pull latest code

git fetch origin main
git reset --hard origin/main
2

Install / update dependencies

npm ci --production=false

Using npm ci (instead of npm install) ensures a clean, reproducible install that matches package-lock.json exactly.

3

Regenerate Prisma client

npm run db:generate

Always run this after pulling, even if you do not see schema changes — the generated client must match the checked-in schema.

4

Apply database migrations

npm run db:migrate:deploy

This applies any pending Prisma migrations to your database in a non-destructive way. It is safe to run on an already up-to-date database.

5

Rebuild the application

npm run build:production
6

Restart the server

# If using PM2
pm2 restart adspot --update-env

# If running manually
npm run start

Automated Deploy Script

The project ships with scripts/deploy.sh, which runs all six steps above in sequence. Use it on any server where the repository is checked out at /var/www/adspot and PM2 manages the process.

# Make executable once
chmod +x scripts/deploy.sh

# Run update
npm run deploy
# or directly:
./scripts/deploy.sh
Zero-downtime tip PM2's --update-env flag reloads environment variables from the process file on restart. If you use the cluster mode (pm2 start ecosystem.config.js), PM2 performs a rolling reload with no dropped requests.

Environment Variable Checklist

Before restarting, compare your .env file against the latest .env.example (or the Environment Setup section). New features often introduce new required variables.

Added WithVariablePurpose
Platform FeesPLATFORM_FEE_RATEOverrides default 15% platform fee (stored in DB settings, no env var required — configure from Admin dashboard)
SMTP / EmailSMTP_HOST, SMTP_USER, SMTP_PASSRequired for notification emails; falls back to Ethereal if missing
SupabaseSUPABASE_URL, SUPABASE_ANON_KEYOptional; only needed if using Supabase for media storage
RedisREDIS_PASSWORDOptional; only needed if Redis caching is enabled

Updating a Docker Deployment

# Pull latest code
git fetch origin main
git reset --hard origin/main

# Rebuild images and restart containers
docker-compose up -d --build

# Migrations run automatically on container start via the entrypoint.
# To run manually inside the container:
docker-compose exec app npm run db:migrate:deploy
Data persistence Docker Compose mounts named volumes for MySQL (mysql_data) and any uploaded media. These volumes are preserved across docker-compose down and rebuild cycles, so your data is safe.

Feature-specific Migration Notes

Some releases introduced database schema changes. If you are upgrading from a version before the date shown, pay attention to the notes below.

Platform Fees & Tax system — 25 Mar 2026

New columns were added to ChargingTransaction to store platformFeeRate, platformFeeAmount, taxRate, taxAmount, and publisherEarning. The migration adds these columns with a default of 0 for existing rows, so no data is lost.

After the migration, navigate to Admin → System Settings to review and confirm the default platform fee (15%) and tax rates (18%). These can be adjusted without any code change.

Run: npm run db:migrate:deploy then npm run db:generate

Screen Concurrency Restriction — 25 Mar 2026

This feature is entirely in-memory (no database changes). No migration is required. After deploying, all new WebSocket connections go through the concurrency check automatically.

If a screen device was already connected before the update, it will remain connected but will be subject to the new concurrency enforcement on its next reconnect.

Offline Ad Support — 24 Mar 2026

The Service Worker (public/sw.js) is a static file shipped with the frontend. No database migration is needed.

After deploying the updated frontend build, browsers will automatically install the new Service Worker on the next page load. Cached media from previous sessions will continue to work.

If you need to force-refresh the cache (e.g. after a cache-naming change), increment the CACHE_NAME constant in public/sw.js before building.

Unit & E2E Testing setup — 21 Mar 2026

Jest, ts-jest, Supertest, and supporting devDependencies were added to package.json. Run npm ci --production=false to install them.

Test configuration lives in test/jest-e2e.config.ts. No environment variables or database changes are required to run the tests — all Prisma operations are mocked.

Verify the setup after upgrading: npm run test:e2e

Rolling Back an Update

If an update causes issues, use the rollback script to revert to a previous commit. Note that database migrations are not automatically reversed — plan accordingly.

# Rollback to the previous commit
npm run rollback
# or directly (optionally pass a specific commit hash):
./scripts/rollback.sh
./scripts/rollback.sh <commit-hash>
Database rollback The rollback script reverts the application code but does not reverse applied migrations. If a migration must be undone, restore from your pre-update database backup rather than attempting to reverse the migration manually.

Post-update Verification

# Check API health
curl http://localhost:3001/api/health

# Full system health (DB + Redis connectivity)
curl http://localhost:3001/api/health/full

# Check migration status
npm run migrate:status

# Run E2E test suite to confirm nothing broke
npm run test:e2e

Database

Migration Commands

# Create a new migration
npm run prisma:migrate

# Apply migrations to production
npm run prisma:migrate:deploy

# Reset database (deletes all data!)
npm run prisma:migrate:reset

# Custom migration scripts
npm run migrate:run
npm run migrate:status
npm run migrate:rollback
npm run migrate:history

Seeding

# Seed admin user
npm run seed:admin

# Seed all data
npm run seed:all

# Clear all seed data
npm run seed:clear

# Reset and reseed
npm run seed:reset

Key Models

ModelDescription
UserUsers with roles, balances, and early bird status
CampaignAdvertising campaigns with scheduling and budgets
ScreenDigital displays with location and heartbeat data
MediaUploaded content with approval workflow
CampaignScreenJunction table linking campaigns to screens
ScreenRatePricing rules with multipliers and peak hours
ChargingTransactionPer-view charge records with rate details
PaymentTransactionGateway payment records (Stripe/PayPal/Razorpay)
TransactionBalance transactions (deposits, charges, refunds)
LanguageSupported languages with RTL configuration
TranslationKey-value translation entries by namespace
ActivityLogAudit trail with IP and user agent tracking

Code Quality

# Run ESLint
npm run lint

# TypeScript type check
npm run check

Frequently Asked Questions

How does campaign billing work?
Campaigns are billed per view using a dynamic rate calculation. The final rate is determined by multiplying the base rate by location and time multipliers. Charges are recorded in 10-second intervals. When a campaign's daily or total budget is exhausted, it automatically pauses to prevent overspend. Daily budgets reset at the configured time each day.
How do I register a new screen?
As a publisher, go to your Screen Management dashboard and create a new screen entry. You'll receive a unique registration code. Enter this code on the physical screen device to link it to your account. Then configure the screen's resolution, orientation, venue type, and location details.
What payment methods are supported?
AdSpot supports Stripe, PayPal, and Razorpay payment gateways. Advertisers can add funds to their account balance using any of these gateways. The admin can configure gateway credentials and switch between test/live modes from the admin dashboard.
How does the media approval process work?
When an advertiser uploads media (images or videos), it enters a "Pending" state. An admin reviews the media and either approves or rejects it with a reason. Only approved media can be used in campaigns. This ensures all content meets platform quality and policy standards.
Can I target specific locations for my campaign?
Yes! Campaigns can target screens by city, region, and venue type (retail, restaurant, cafe, gym, transit, office, hospital, mall, hotel). You can also use timeslot scheduling to control when your ads appear on specific days and hours.
What is the Early Bird program?
The Early Bird program offers special benefits to users who join during the prelaunch period. Benefits include reduced platform fees (5% vs 15%), higher publisher revenue share (85% vs 70%), $25 trial credit, and no setup fees. The program runs for 12 months from enrollment.
How does screen heartbeat monitoring work?
Active screens periodically send heartbeat signals to the server, updating their last_active timestamp. This allows the platform to track which screens are online or offline in real-time. Publishers can monitor screen status from their dashboard and receive alerts about connectivity issues.
Does AdSpot support multiple languages?
Yes. AdSpot has a comprehensive i18n system with database-backed translations. It supports RTL languages (Arabic, Hebrew, etc.) with automatic layout direction switching. Admins can manage languages, translations, and track translation completion status from the admin panel.
How are publisher earnings calculated?
Publishers earn revenue based on views displayed on their screens. The revenue share is determined by the platform fee (default 15%, or 5% for early bird users). Publishers can view their earnings, track performance per screen, and request payouts via bank transfer from their dashboard.
What are the deployment options?
AdSpot supports three deployment options: (1) Docker Compose for self-hosted environments with MySQL and Redis containers, (2) Vercel for serverless deployment using the included vercel.json configuration, and (3) standalone Docker containers for custom infrastructure. The project includes a multi-stage Dockerfile for optimized production builds.
How do I run database migrations?

Use Prisma CLI commands: npm run prisma:migrate to create a new migration, npm run prisma:migrate:deploy to apply migrations to production. After any schema change, always run npm run prisma:generate to regenerate the Prisma client.

Can the landing page be customized without code changes?
Yes! AdSpot includes a full landing page CMS. Admins can edit hero slides, benefits, features, testimonials, upcoming features, and how-it-works steps directly from the admin dashboard. Content changes are stored in the database and reflected immediately on the public landing page.
What happens when a screen loses internet connectivity?
Screens continue playing ads uninterrupted thanks to the Service Worker media cache. When connectivity drops, previously cached images and videos are served locally. The Service Worker uses a network-first strategy, so fresh content is always preferred when online and the cache is updated automatically. Video assets support byte-range requests from cache, enabling smooth playback. When connectivity is restored, the WebSocket heartbeat reconnects and billing resumes normally.
How does AdSpot prevent double-billing on screens?
AdSpot enforces exactly one active WebSocket session per screen. When a screen authenticates, the gateway checks for an existing connection on the same screen. If one is found, the new connection is rejected with a screen:already_in_use event. Since charging only occurs on heartbeat events from the single authorised socket, it is impossible for the same screen to be billed twice for the same interval.
How is the publisher's net earning calculated?
Every charging transaction records the gross amount charged to the advertiser. The platform then deducts a platform fee (default 15%, or 5% for early-bird advertisers) and a tax on that fee (default 18%). The remainder is the publisher's net earning. The Publisher Dashboard shows a full breakdown: Gross Revenue, Platform Fee, Tax on Fee, and Net Earnings. All rate details are stored with each transaction record for complete auditability.
How do I run the automated tests?
Run npm run test:e2e to execute all 18 backend E2E test suites. Use npm run test:e2e:report to generate an HTML report at test/reports/e2e-test-report.html. Frontend unit tests using Jest and React Testing Library are colocated with their components and can be run via the standard npm test command.
Can I white-label AdSpot with my own brand?
Yes. All brand-facing elements are controlled from the Admin dashboard — no code changes required. You can update the business name, upload a logo, and set a contact email. Changes propagate immediately to the landing page, all user dashboards, and outgoing email notifications. Every user role can also upload a personal profile avatar that persists across sessions.
How do I change the platform currency?
Go to Admin → Settings → Currency and select the desired primary currency. All monetary displays across Admin, Advertiser, and Publisher dashboards update immediately. Payment gateway checkout sessions are created with the configured currency code. It is recommended to set the currency before any live transactions, as existing records store raw numbers without an embedded currency code.
Where are activity logs stored and how long are they kept?
Activity logs capture every user action with IP address, user agent, timestamp, and action type. The Admin can configure the log destination (database, file, or cloud) and the retention period from Admin → Settings → Logging. Logs can be reviewed and filtered from the Admin audit trail panel.