This content originally appeared on DEV Community and was authored by gnetid
TL;DR
I built a complete multi-tenant ISP billing system from scratch and deployed it to production in just 48 hours using Qoder AI. The system now handles real ISP operations including:
Multi-tenant architecture with full data isolation
PPPoE user management & FreeRADIUS integration
Payment processing (Midtrans & Xendit)
WhatsApp notifications with load balancing
Hotspot voucher system with e-commerce
Real-time router monitoring
Financial tracking & reporting
Tech Stack: Next.js 15, Prisma ORM, FreeRADIUS, Mikrotik RouterOS, MySQL
The Challenge
ISP billing systems are notoriously complex. You need to handle:
- Customer management across multiple branches
- Real-time integration with network equipment (Mikrotik routers)
- Payment gateway integration with webhook handling
- Automated notifications (WhatsApp, Email)
- Multi-tenant data isolation
- Financial reporting
Traditionally, building such a system would take weeks or months. I had 2 days.
Why Qoder AI?
I’ve used various AI coding assistants before, but Qoder’s memory feature changed the game completely. Here’s what made the difference:
1. Context Retention Across Sessions
Qoder remembered our previous conversations about the architecture. When I came back the next day, it didn’t ask “what are you building?” – it just continued where we left off.
2. Understanding Complex Architecture
Multi-tenant systems are tricky. Qoder helped me:
- Design proper tenant isolation in the database
- Implement domain-based tenant detection
- Handle centralized services with tenant overrides (VPN, Payment Gateway)
3. Production-Ready Code
Not just prototypes – Qoder helped write actual production code with:
- Proper error handling
- Security considerations (tenant isolation, authentication)
- Performance optimizations
- Real-world edge cases
The Architecture
Multi-Tenant Design
The system supports two operational modes:
HEAD Office:
// SuperHead admin manages all tenants
// Access: /mgmt
// Role: HEAD_ADMIN
// tenantId: null
Branch Offices:
// Each tenant has isolated data
// Access: /admin
// Role: BRANCH_ADMIN
// tenantId: specific UUID
Database Schema Highlights
// All core models have tenant isolation
model PppoeUser {
id String @id @default(uuid())
tenantId String? // NULL = shared, UUID = specific tenant
username String @unique
// ... other fields
tenant Tenant? @relation(fields: [tenantId], references: [id])
@@index([tenantId])
}
model PaymentGateway {
id String @id @default(uuid())
tenantId String? // NULL = HEAD (central), UUID = tenant override
provider String // midtrans, xendit, duitku
// ... credentials
@@unique([tenantId, provider])
}
Key Technical Decisions
1. Centralized Services with Override Capability
Some services (VPN Server, Payment Gateway) are shared by default but can be overridden per tenant:
// Payment Gateway Selection Logic
async function getActivePaymentGateway(provider: string, tenantId: string | null) {
// Check tenant override flag
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { useCustomPaymentGw: true }
})
if (tenant?.useCustomPaymentGw) {
// Use tenant-specific gateway
return await prisma.paymentGateway.findFirst({
where: { provider, tenantId, isActive: true }
})
}
// Fallback to HEAD (central) gateway
return await prisma.paymentGateway.findFirst({
where: { provider, tenantId: null, isActive: true }
})
}
2. Domain-Based Tenant Detection
Public-facing pages (e-voucher store, customer portal) auto-detect tenant from domain:
export async function GET(req: NextRequest) {
const host = req.headers.get('host') || ''
const domain = host.split(':')[0].toLowerCase()
const tenant = await prisma.tenant.findFirst({
where: {
OR: [
{ domain: domain },
{ alternativeDomain: domain }
],
isActive: true
}
})
// Filter data by detected tenant
const profiles = await prisma.hotspotProfile.findMany({
where: {
tenantId: tenant?.id,
eVoucherAccess: true
}
})
}
3. Webhook Handler with Tenant Auto-Detection
Single webhook endpoint serves all tenants – auto-detects from invoice/order:
// Centralized webhook: /api/payment/webhook
export async function POST(req: NextRequest) {
// 1. Detect payment gateway
const gateway = detectGateway(req)
// 2. Extract order ID
const orderId = extractOrderId(body, gateway)
// 3. Auto-detect tenant from invoice/order
let tenantId: string | null = null
if (orderId.startsWith('EV')) {
// E-voucher order
const order = await prisma.hotspotVoucherOrder.findFirst({
where: { orderNumber: orderId },
select: { tenantId: true }
})
tenantId = order?.tenantId || null
} else {
// Invoice payment
const invoice = await prisma.invoice.findUnique({
where: { paymentToken: orderId },
select: { tenantId: true }
})
tenantId = invoice?.tenantId || null
}
// 4. Get tenant-aware payment gateway config
const gatewayConfig = await getActivePaymentGateway(gateway, tenantId)
// 5. Verify signature & process payment
// ...
}
Development Journey
Day 1: Core Foundation (12 hours)
Morning (4 hours):
Set up Next.js 15 project with Prisma
Design multi-tenant database schema
Implement authentication (NextAuth with dual admin types)
Create SuperHead dashboard for tenant management
Afternoon (4 hours):
PPPoE user management UI
FreeRADIUS integration for authentication
Mikrotik RouterOS API integration
Real-time session monitoring
Evening (4 hours):
Payment gateway integration (Midtrans & Xendit)
Invoice generation system
Webhook handler (centralized, tenant-aware)
Day 2: Integration & Polish (12 hours)
Morning (4 hours):
WhatsApp notification system with load balancing
Hotspot voucher generation
E-voucher public store with payment
VPN client management
Afternoon (4 hours):
Financial tracking (Keuangan module)
Multi-tenant payment gateway override
Domain-based tenant detection
Tenant-specific redirect URLs
Evening (4 hours):
Production deployment
Testing with real Mikrotik routers
Live payment testing
Bug fixes & optimizations
How Qoder AI Accelerated Development
1. Architectural Guidance
When designing the multi-tenant architecture, I asked Qoder about the VPN server pattern from a previous project. It remembered the implementation and suggested:
“Om, untuk VPN server bisa pakai pattern centralized dengan tenant override capability. VPN Server dibuat shared (tenantId = null), tapi setiap tenant punya VPN Client sendiri.”
This saved hours of architectural decision-making.
2. Code Generation with Context
Qoder didn’t just generate isolated code snippets. It understood the entire codebase structure:
Me: “Add payment gateway override toggle in admin panel”
Qoder generated:
API endpoint: /api/tenant/payment-gateway-override
Database query with proper tenant filter
UI component with toggle switch
Integration with existing payment flow
Updated helper functions to respect override flag
All in one response, fully integrated with existing code!
3. Bug Hunting & Fixes
When I encountered a Prisma error with compound unique keys:
Error: Type '{ provider: string; }' is not assignable to type 'PaymentGatewayWhereUniqueInput'
Qoder immediately identified the issue and provided the fix:
// Before (broken)
where: { provider: 'midtrans' }
// After (fixed)
where: {
tenantId_provider: {
tenantId: tenantId,
provider: 'midtrans'
}
}
4. Production Best Practices
Qoder proactively suggested production improvements:
- “Add tenantId index for faster queries”
- “Use transaction for payment webhook to avoid race conditions”
- “Implement exponential backoff for WhatsApp API retries”
- “Add domain validation before tenant creation”
These weren’t things I asked for – Qoder anticipated production needs!
Code Examples
Example 1: Tenant-Aware API Endpoint
// app/api/pppoe/users/route.ts
export async function GET() {
const session = await getServerSession(authOptions)
// Build tenant filter
const tenantWhere = session.user.role === 'HEAD_ADMIN'
? {} // HEAD sees all tenants
: { tenantId: session.user.tenantId } // Branch sees only their data
const users = await prisma.pppoeUser.findMany({
where: {
...tenantWhere,
status: 'ACTIVE'
},
include: {
profile: true,
router: true
}
})
return NextResponse.json({ users })
}
Example 2: WhatsApp Load Balancing
// lib/whatsapp-service.ts
async function selectProvider(tenantId: string | null): Promise<WhatsAppProvider> {
const providers = await prisma.whatsappProvider.findMany({
where: {
tenantId: tenantId,
isActive: true
}
})
// Calculate current usage for each provider
const now = new Date()
const oneMinuteAgo = new Date(now.getTime() - 60000)
const providerUsage = await Promise.all(
providers.map(async (provider) => {
const recentCount = await prisma.whatsAppHistory.count({
where: {
providerId: provider.id,
createdAt: { gte: oneMinuteAgo }
}
})
return {
provider,
usage: recentCount,
capacity: provider.maxPerMinute
}
})
)
// Select provider with lowest usage ratio
return providerUsage
.filter(p => p.usage < p.capacity)
.sort((a, b) => (a.usage / a.capacity) - (b.usage / b.capacity))[0]
?.provider
}
Results & Impact
Production Metrics (After 1 Month)
- 6 Tenants actively using the system
- 500+ PPPoE Users managed across tenants
- 1,000+ Invoices processed
- 10,000+ Hotspot Vouchers generated
- 100% Payment Success Rate (webhook handling)
- 99.9% Uptime (real-time monitoring with alerts)
Business Value
- Rapid Deployment: ISP branches can onboard in < 5 minutes
- Cost Savings: No per-tenant infrastructure needed
- Centralized Management: HEAD office monitors all branches
- Flexibility: Branches can customize payment gateway & WhatsApp
- Scalability: Add unlimited tenants without code changes
Technical Achievements
Complete tenant isolation (data, domains, services)
Real-time FreeRADIUS integration
Multi-gateway payment processing
Automated WhatsApp notifications with load balancing
VPN-based router management
Public e-voucher store with payment
Lessons Learned
What Worked Amazingly Well
1. Qoder’s Memory Feature
The ability to continue conversations across sessions was a game-changer. I could work in short bursts (2-3 hours) and Qoder always picked up exactly where we left off.
2. Multi-Tenant from Day 1
Designing for multi-tenancy from the start (even for a single client) proved invaluable. When new branches wanted to use the system, it was just “create tenant” – done!
3. Centralized Services with Override
The VPN Server and Payment Gateway pattern (shared by default, override per tenant) gave perfect balance between simplicity and flexibility.
4. Domain-Based Tenant Detection
Auto-detecting tenant from domain eliminated configuration complexity for public-facing features (e-voucher store, customer portal).
Challenges & Solutions
Challenge 1: Prisma Compound Unique Keys
-
Issue: Couldn’t use
where: { provider }due to@@unique([tenantId, provider]) -
Solution: Use compound key syntax or
findFirstwith full where clause - Lesson: Always check Prisma schema for compound uniques!
Challenge 2: FreeRADIUS Restart Required
- Issue: Router changes didn’t reflect until FreeRADIUS restarted
-
Solution: Automated
sudo systemctl restart freeradiusafter NAS changes - Lesson: External service integration needs automation!
Challenge 3: Webhook Tenant Detection
- Issue: Single webhook URL for all tenants – how to know which tenant?
- Solution: Auto-detect from invoice/order → get tenant-specific config
- Lesson: Design webhooks for multi-tenancy from the start!
Qoder Pro Tips
- Use Memory Wisely: Tell Qoder to remember important architectural decisions
- Show Context: Share relevant code when asking for changes
- Iterate in Chunks: Break features into smaller tasks for better results
- Ask for Explanations: Understanding the “why” helps catch issues early
5. Production Mindset: Ask about error handling, edge cases, security
What’s Next?
The system is live and stable, but there’s always room for improvement:
Planned Features:
Advanced analytics dashboard (revenue trends, user growth)
Automated customer onboarding via chatbot
Mobile app for admins and agents
Telegram bot for real-time alerts
Multi-language support
Automated database backups with Telegram integration
Technical Improvements:
- Redis caching for session data
- Elasticsearch for invoice search
- Webhook retry queue with Bull
- API rate limiting per tenant
Conclusion
Building a production-ready ISP billing system in 2 days seemed impossible. With Qoder AI, it became reality.
The key wasn’t just AI-generated code – it was having an AI assistant that:
- Remembered our architectural decisions
- Understood the business context
- Suggested production-ready patterns
- Caught errors before they hit production
- Stayed consistent across long development sessions
For developers building complex, multi-tenant systems – Qoder’s memory feature is a superpower. It’s like pair programming with someone who never forgets, never gets tired, and always has production best practices in mind.
Huge thanks to Nathan Steel and the Qoder team for building such an incredible tool! 
Links & Resources
- Project Type: ISP Billing & Management System
- Tech Stack: Next.js 15, Prisma, FreeRADIUS, Mikrotik
- Deployment: Production (6 active tenants)
- Development Time: 48 hours
- AI Assistant: Qoder AI
Want to discuss multi-tenant architecture or Qoder tips? Drop a comment below! 
Tags: #qoder #nextjs #typescript #prisma #multitenant #casestudy #isp #billing #production
This content originally appeared on DEV Community and was authored by gnetid