
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.
Smart Search: I implemented an insensitive search for titles and positions, making it easy to manage hundreds of ads.
Pagination: As the system grows, we don't want to load 1,000 ads at once. The API uses skip and take to deliver data in small, fast chunks.
JavaScript
// Security: Only fetch data for the specific client's site
const where = {
siteId: session.user.siteId,
};
4. Pros of This System (Why it beats the competition)
A. Total Versatility
Whether you want to run Google AdSense (via the html or script fields) or a Custom Affiliate Banner (via image and linkUrl), this system handles it. You aren't locked into one provider.
B. High Performance (Next.js 15 Optimization)
Because this is built on Next.js, I can use Server Components to fetch the ad data on the server side. This means the ad is "ready" when the HTML reaches the user, reducing the "jumpy" feeling of late-loading ads.
C. The ads.txt Integration
One of the most unique features is the ability to manage the ads.txt file via the admin panel. For professional publishers, ads.txt is mandatory for verification. My system allows the owner to update this without touching the code or FTP.
D. Targeted Advertising
You can target a specific blog post by its Slug. If you have a post about "Visa Services," you can display a "Visa Consultant" ad specifically on that page while showing "Tech" ads elsewhere.
5. How it Works: The Lifecycle of an Ad
Creation: The Admin logs in, selects the page (e.g., "Blog Post"), the position (e.g., "Content-Top"), and uploads the banner.
Storage: Prisma saves the record with a
siteIdand setsisActive: true.Injection: In the Next.js frontend, a layout component queries the API: "Give me the active ad for 'post' at 'content-top'."
Verification: The system checks the
startDateandendDateto ensure the ad is still valid.Rendering: The ad is rendered. If it’s a
CUSTOMtype, the raw HTML/Script is injected safely. If it’s aBANNER, it shows a beautiful optimized Next.js Image.
6. Personal Reflection: Coding Between the Rows
Building this wasn't easy. While working on the farm in Saudi Arabia, I don't have the luxury of a 24/7 high-speed fiber connection or a designer chair. I have my laptop, my logic, and a goal.
Every line of code in this Advertisement System represents a step toward a professional career. When I coded the @@index([siteId, pageType, position]) in Prisma, I wasn't just thinking about database speed—I was thinking about the efficiency of a system that can handle thousands of users.
Why this is "Best"?
It’s "best" because it’s Human-Centric. It doesn't treat ads as an afterthought. It treats them as a core part of the content strategy. It gives power back to the creator.
Conclusion
This system is a testament to what Next.js 15 and Prisma can do when pushed to the limit. It’s secure, it’s blazing fast, and it’s built for the modern web.
If you are a business owner looking for a custom solution, or a developer looking for inspiration—don't settle for the "standard" way. Build a system that gives you control.





