
Beyond AdSense: Building a Multi-Tenant Ads Management System with Next.js 15 & Prisma
Why I Built a Custom Multi-Tenant Advertisement System with Next.js 15 and Prisma
In the world of web development, we are often told to "just use Google AdSense." But as I worked on my project—balancing 10-hour shifts on a farm in Saudi Arabia and coding during my late-night hours—I realized that true freedom comes from building your own infrastructure.
Today, I want to pull back the curtain on the Advertisement System I developed for WiseMix Media. It’s not just a script; it’s a full-stack, database-driven engine built using Next.js 15, Prisma, and PostgreSQL.
The Problem: Why "Default" Ads Aren't Enough
Most developers rely on third-party ad networks. However, these networks come with limitations:
Lack of Control: You can’t easily toggle between affiliate banners, direct sponsor images, and video ads.
Speed Issues: Heavy third-party scripts can destroy your LCP (Largest Contentful Paint) scores.
Layout Shifts: Unpredictable ad sizes lead to frustrating layout shifts for users.
I wanted a system where I—the admin—controlled every pixel. Whether it’s a manual banner for a local partner or a custom ads.txt file for verification, I wanted it all in one dashboard.
1. The Blueprint: Designing the Schema with Prisma
The heart of any robust system is its data model. I used Prisma to define a flexible Advertisement model that supports multiple types of media and precise targeting.
Code snippet
enum AdType {
AFFILIATE
LINK
BANNER
POPUP
VIDEO
CUSTOM
}
model Advertisement {
id String @id @default(cuid())
siteId String // Multi-tenant support
title String
adType AdType @default(CUSTOM)
html String? // For Google AdSense or Custom Scripts
linkUrl String? // For Banner/Link clicks
image String? // For Image banners
script String?
pageType String // home | post | category | tool
pageSlug String? // Targeted specific posts
position String // sidebar-top | content-top | content-bottom
isActive Boolean @default(true)
impressions Int @default(0)
clicks Int @default(0)
owner User @relation(fields: [siteId], references: [siteId])
createdAt DateTime @default(now())
}
Why this Prisma model is "Best in Class":
Multi-Tenancy (
siteId): The system is built for scale. Multiple website owners can manage their own ads within the same database, isolated by theirsiteId.Granular Positioning: Most systems only allow "Top" or "Bottom." My system supports
content-middle,sidebar-top, and even specificpageSlugtargeting.Performance Tracking: With
impressionsandclicksfields, I can build an analytics dashboard to show ROI (Return on Investment) to sponsors.
2. The Admin Experience: A "Use Client" Masterpiece
The frontend of the Ad Manager is built for speed and ease of use. I used Next.js Client Components to create a dynamic form where the "Position" options change based on the "Page Type" selected.
Dynamic Logic in the Admin UI:
In my code, I mapped specific positions to specific pages. For example, the Homepage might have a sidebar-bottom, but a Blog Post might focus more on content-middle for higher engagement.
JavaScript
const POSITIONS = {
home: ['content-top', 'content-middle', 'content-bottom', 'sidebar-top', 'sidebar-bottom'],
post: ['content-top', 'content-middle', 'content-bottom'],
tool: ['content-top', 'content-bottom', 'sidebar-top', 'sidebar-bottom'],
};
This ensures that the admin (the website owner) cannot make a mistake by placing a "sidebar" ad on a page that doesn't have a sidebar. It’s "fail-proof" by design.
3. The API Layer: Secure and Scalable
The backend API handles the heavy lifting. I implemented strict security checks using getServerSession from NextAuth.
Key Features of the API:
Security First: The
GETrequest specifically filters bysiteId. This means even if a malicious user tries to guess an ID, the API only returns data belonging to the logged-in user.





