A full-stack TypeScript SaaS platform for managing digital advertisements across distributed display networks in real time.
AdMiroTS is built for organizations that need to create, schedule, distribute, and monitor digital advertisements across multiple screens, kiosks, and signage devices.
The system provides:
- centralized advertisement management
- display device registration and control
- playlist (display loop) orchestration
- real-time playback synchronization
- analytics and engagement tracking
- complete activity logging for auditability
- role-based access control for multi-user operations
This repository contains a split monorepo architecture for the full production system with strict TypeScript modeling and domain-driven structure. frontend/ and backend/ are independently hostable units.
AdMiro solves end-to-end digital signage operations:
- Content teams upload image/video ads with metadata and lifecycle status.
- Operations teams register and manage display devices with health/status tracking.
- Campaign managers build loops (playlists), control ordering/rotation, and assign loops to displays.
- Display clients fetch assigned content, play ads, and report status/events.
- The platform tracks impressions, interactions, and uptime metrics for analytics.
- Every critical action is recorded in an immutable activity log for compliance and debugging.
- Authentication with JWT and refresh token flow
- Google OAuth sign-in support
- Role-based authorization (Admin, Advertiser)
- Advertisement CRUD with media validation and status lifecycle
- Display management with connection token flow
- Loop creation with sequential/random rotation and layout strategy
- Display heartbeat and refresh polling workflow
- Analytics across displays, loops, and advertisements
- Audit trail with filtering by action, entity, user, and date range
Browser / Display Client
|
v
Next.js Frontend (frontend)
|
v
Express API (backend)
- Auth + RBAC
- Domain Controllers/Services
- Validation + Middleware
- Logging + Analytics
|
v
MongoDB (7 primary collections)
.
├── backend/
│ ├── src/ # Express API source
│ ├── api/index.ts # Vercel API entrypoint
│ └── packages/
│ ├── domain/ # backend domain entities/enums/interfaces
│ └── shared/ # backend DTOs/types/schemas
├── frontend/
│ ├── src/
│ │ ├── app/ # Next.js App Router pages
│ │ ├── features/ # feature-first UI domain code
│ │ ├── components/ # reusable UI pieces
│ │ ├── context/ # auth/session state management
│ │ └── lib/
│ │ ├── api/ # API client modules
│ │ └── contracts/ # frontend-local copied contracts (Zod/types)
│ └── public/
└── docs/
├── PROJECT_DESCRIPTION.md
└── MONOREPO_WORKFLOW_STRUCTURE.md
The backend is organized around strongly typed domain entities.
Primary entities:
- User
- Advertisement
- Display
- DisplayLoop
- SystemLog
- Analytics
- DisplayConnectionRequest
Typical interface shape:
export interface Advertisement {
id: string;
adId: string;
advertiserId: string;
adName: string;
mediaUrl: string;
mediaType: "image" | "video";
duration: number;
status: "active" | "scheduled" | "paused" | "expired" | "draft";
createdAt: string;
updatedAt: string;
}export interface DisplayLoop {
id: string;
loopId: string;
displayId: string;
loopName: string;
rotationType: "sequential" | "random" | "weighted";
displayLayout: "fullscreen" | "masonry";
advertisements: Array<{ adId: string; loopOrder: number }>;
totalDuration: number;
}Service layer classes follow explicit responsibilities:
- AuthService: token issuance, refresh, credential/OAuth auth
- AdvertisementService: ad lifecycle and media constraints
- DisplayService: registration, status, assignment, refresh triggers
- DisplayLoopService: ordering, rotation, duration calculation
- AnalyticsService: aggregation and query APIs
- LoggingService: audit creation and retrieval
This separation keeps controllers thin, services testable, and repository/data access isolated.
The platform exposes REST APIs grouped by domain:
- /api/auth
- /api/advertisements
- /api/displays
- /api/display-loops
- /api/system-logs
- /api/profile
- /api/analytics
Design principles:
- consistent success/error response envelopes
- pagination and filtering for list endpoints
- ownership checks for advertiser-scoped resources
- admin-only gates for privileged actions
- strict input validation and typed request DTOs
The data access layer follows the Repository Pattern to provide a consistent interface for interacting with MongoDB collections. The BaseRepository class encapsulates common CRUD operations, pagination, and filtering logic that all concrete repositories inherit. Concrete repositories—including AdvertisementRepository, DisplayRepository, and UserRepository—extend this base to add domain-specific queries, custom mappings, and business logic tailored to their entities. This approach centralizes data access concerns, improves testability through dependency injection, and enforces a clear separation of concerns between domain logic and data persistence.
Frontend uses Next.js App Router with feature-first organization:
- authenticated dashboard area for management workflows
- public display playback and display authentication pages
- domain-driven UI slices for Ads, Displays, Loops, Logs, Analytics, Profile
- centralized API client and typed contracts
- predictable state management for auth and session context
- Helmet and CORS hardening
- Rate limiting for general and auth-sensitive routes
- JWT verification middleware and role checks
- File upload constraints (size/type) and Cloudflare R2 signed URLs
- Global error handling with normalized error payloads
- Structured logging for traceability and operations
- Node.js 20+
- npm 10+
cd backend
npm install
npm run dev
npm run build
npm run typecheck
npm run testcd frontend
npm install
npm run dev
npm run build
npm run typecheck
npm run test:contracts- Backend runtime uses
backend/.env. - Frontend runtime uses
frontend/.env.local. - Keep these values aligned:
frontend/.env.local:NEXT_PUBLIC_API_BASE_URL-> backend URL (defaulthttp://localhost:8000)frontend/.env.local:NEXT_PUBLIC_GOOGLE_CLIENT_ID==backend/.env:GOOGLE_CLIENT_ID
- For local CORS, include both:
http://localhost:3000http://127.0.0.1:3000
- Ensure R2 values are populated in backend env if you use file uploads.
- Deploy
frontendas a Vercel Next.js project (root directory:frontend). - Deploy
backendas a separate Vercel project (root directory:backend) usingbackend/vercel.json. - Frontend env file:
frontend/.env.localNEXT_PUBLIC_API_BASE_URLNEXT_PUBLIC_GOOGLE_CLIENT_ID
- Backend env file:
backend/.envMONGODB_URI,JWT_SECRET,JWT_EXPIRES_INGOOGLE_CLIENT_ID,CORS_ORIGINSR2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_BUCKET_NAME,R2_PUBLIC_BASE_URL,R2_UPLOAD_URL_TTL_SECONDS
If browser console shows GSI_LOGGER: The given origin is not allowed for the given client ID:
- Ensure
frontend/.env.localhasNEXT_PUBLIC_GOOGLE_CLIENT_ID. - Ensure
backend/.envhasGOOGLE_CLIENT_ID. - Both values must be the same Google OAuth Web application client ID.
- In Google Cloud Console for that client ID, add your exact frontend origin to Authorized JavaScript origins.
- Local example:
http://localhost:3000 - Also add
http://127.0.0.1:3000if you open frontend via 127.0.0.1 - Include the port if not 80/443.
- Do not include path/query/hash.
- Local example:
- Restart frontend and backend dev servers after changing env vars.
- Contract matrix:
docs/CONTRACT_CONFORMANCE_MATRIX.md - Local smoke checklist:
docs/LOCAL_SMOKE_CHECKLIST.md - Audit report template:
docs/AUDIT_PASS_REPORT_TEMPLATE.md
- Admin signs in and registers displays.
- Advertiser uploads media and creates advertisements.
- Advertiser creates display loops and assigns ads.
- Admin assigns a loop to a target display.
- Display client fetches loop content and starts playback.
- Client reports heartbeat + play metrics.
- Dashboard visualizes analytics and logs in near real time.
- Retail promotions across store networks
- Transportation hub ad scheduling
- Corporate internal signage
- Campus communication systems
- Multi-location campaign rollout with centralized control