Northland Tech Solutions - Technical Specification
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ PUBLIC WEBSITE │
│ (Marketing, Services, About, Contact, Blog) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CLIENT PORTAL │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Tickets │ │ Billing │ │ Docs │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ INTERNAL CRM │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Clients │ │ Tasks │ │ Scheduler │ │
│ │ Database │ │ Queue │ │ (Agent) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Tech Stack
Frontend
Component Technology Rationale Framework Next.js 14+ (App Router) Full-stack, SEO, performance Styling Tailwind CSS Rapid development, professional theme Components shadcn/ui Polished, accessible components Charts Recharts or Chart.js Dashboard visualizations Forms React Hook Form + Zod Validation, UX
Backend
Component Technology Rationale API Next.js API Routes Unified codebase Database PostgreSQL Relational, robust ORM Prisma Type-safe, migrations Auth Clerk or NextAuth.js Multi-tenant portal auth Email Resend Transactional emails File Storage S3/Cloudflare R2 Documents, attachments
CRM/Automation
Component Technology Rationale Task Queue BullMQ + Redis Background jobs, scheduling Agentic Scheduler Custom + LLM integration Smart task prioritization SLA Engine Custom logic Escalation, notifications Notifications Email + SMS (Twilio?) Multi-channel alerts
Database Schema
Core Entities
// Organizations (Clients)
model Organization {
id String @id @default ( cuid ())
name String
slug String @unique
industry String ?
website String ?
phone String ?
address Json ?
notes String ? @db.Text
status OrgStatus @default ( PROSPECT )
slaLevel SlaLevel @default ( STANDARD )
createdAt DateTime @default ( now ())
updatedAt DateTime @updatedAt
contacts Contact []
contracts Contract []
tickets Ticket []
invoices Invoice []
assets Asset []
tasks Task []
}
enum OrgStatus {
PROSPECT
ACTIVE
INACTIVE
CHURNED
}
enum SlaLevel {
BASIC // 24hr response
STANDARD // 8hr response
PRIORITY // 4hr response
CRITICAL // 1hr response
}
// Contacts within organizations
model Contact {
id String @id @default ( cuid ())
organization Organization @relation ( fields : [ orgId ], references : [ id ])
orgId String
firstName String
lastName String
email String @unique
phone String ?
role String ?
isPrimary Boolean @default ( false )
portalAccess Boolean @default ( false )
portalPassword String ? // Hashed
createdAt DateTime @default ( now ())
tickets Ticket []
}
// Service Contracts
model Contract {
id String @id @default ( cuid ())
organization Organization @relation ( fields : [ orgId ], references : [ id ])
orgId String
type ContractType
monthlyValue Decimal @db.Decimal ( 10 , 2 )
startDate DateTime
endDate DateTime ?
autoRenew Boolean @default ( true )
terms String ? @db.Text
status ContractStatus @default ( ACTIVE )
createdAt DateTime @default ( now ())
}
enum ContractType {
MSP_RETAINER
HOURLY_SUPPORT
PROJECT
CABLING
VPS_HOSTING
}
enum ContractStatus {
DRAFT
ACTIVE
EXPIRED
CANCELLED
}
// Support Tickets
model Ticket {
id String @id @default ( cuid ())
organization Organization @relation ( fields : [ orgId ], references : [ id ])
orgId String
contact Contact ? @relation ( fields : [ contactId ], references : [ id ])
contactId String ?
subject String
description String @db.Text
priority Priority @default ( MEDIUM )
status TicketStatus @default ( OPEN )
category String ?
slaDeadline DateTime ?
assignedTo String ? // Staff user ID
resolvedAt DateTime ?
createdAt DateTime @default ( now ())
updatedAt DateTime @updatedAt
comments TicketComment []
timeEntries TimeEntry []
}
enum Priority {
LOW
MEDIUM
HIGH
URGENT
}
enum TicketStatus {
OPEN
IN_PROGRESS
WAITING_CLIENT
WAITING_VENDOR
RESOLVED
CLOSED
}
model TicketComment {
id String @id @default ( cuid ())
ticket Ticket @relation ( fields : [ ticketId ], references : [ id ])
ticketId String
authorId String // Staff or contact ID
authorType String // 'staff' or 'client'
content String @db.Text
internal Boolean @default ( false ) // Hidden from client
createdAt DateTime @default ( now ())
}
// Time Tracking
model TimeEntry {
id String @id @default ( cuid ())
ticket Ticket ? @relation ( fields : [ ticketId ], references : [ id ])
ticketId String ?
task Task ? @relation ( fields : [ taskId ], references : [ id ])
taskId String ?
staffId String
description String
minutes Int
billable Boolean @default ( true )
date DateTime @default ( now ())
}
// Tasks (Internal & Client-facing)
model Task {
id String @id @default ( cuid ())
organization Organization ? @relation ( fields : [ orgId ], references : [ id ])
orgId String ?
title String
description String ? @db.Text
priority Priority @default ( MEDIUM )
status TaskStatus @default ( TODO )
dueDate DateTime ?
assignedTo String ?
recurring Boolean @default ( false )
recurrenceRule String ? // RRULE format
agentGenerated Boolean @default ( false )
createdAt DateTime @default ( now ())
updatedAt DateTime @updatedAt
timeEntries TimeEntry []
}
enum TaskStatus {
TODO
IN_PROGRESS
BLOCKED
DONE
CANCELLED
}
// Client Assets (Hardware, Software, Services)
model Asset {
id String @id @default ( cuid ())
organization Organization @relation ( fields : [ orgId ], references : [ id ])
orgId String
name String
type AssetType
manufacturer String ?
model String ?
serialNumber String ?
purchaseDate DateTime ?
warrantyEnd DateTime ?
location String ?
ipAddress String ?
notes String ? @db.Text
status AssetStatus @default ( ACTIVE )
createdAt DateTime @default ( now ())
}
enum AssetType {
WORKSTATION
SERVER
NETWORK
PRINTER
PHONE
SOFTWARE
CLOUD_SERVICE
OTHER
}
enum AssetStatus {
ACTIVE
INACTIVE
DECOMMISSIONED
MAINTENANCE
}
// Invoices
model Invoice {
id String @id @default ( cuid ())
organization Organization @relation ( fields : [ orgId ], references : [ id ])
orgId String
number String @unique
issueDate DateTime @default ( now ())
dueDate DateTime
subtotal Decimal @db.Decimal ( 10 , 2 )
tax Decimal @default ( 0 ) @db.Decimal ( 10 , 2 )
total Decimal @db.Decimal ( 10 , 2 )
status InvoiceStatus @default ( DRAFT )
paidAt DateTime ?
stripeInvoiceId String ?
items InvoiceItem []
}
enum InvoiceStatus {
DRAFT
SENT
PAID
OVERDUE
VOID
}
model InvoiceItem {
id String @id @default ( cuid ())
invoice Invoice @relation ( fields : [ invoiceId ], references : [ id ])
invoiceId String
description String
quantity Decimal @db.Decimal ( 10 , 2 )
unitPrice Decimal @db.Decimal ( 10 , 2 )
total Decimal @db.Decimal ( 10 , 2 )
}
SLA Engine Logic
// SLA deadlines based on priority + client SLA level
const SLA_MATRIX = {
BASIC: {
LOW: 48 , // hours
MEDIUM: 24 ,
HIGH: 12 ,
URGENT: 8 ,
},
STANDARD: {
LOW: 24 ,
MEDIUM: 8 ,
HIGH: 4 ,
URGENT: 2 ,
},
PRIORITY: {
LOW: 8 ,
MEDIUM: 4 ,
HIGH: 2 ,
URGENT: 1 ,
},
CRITICAL: {
LOW: 4 ,
MEDIUM: 2 ,
HIGH: 1 ,
URGENT: 0.5 ,
},
};
function calculateSlaDeadline (
priority : Priority ,
slaLevel : SlaLevel ,
createdAt : Date
) : Date {
const hours = SLA_MATRIX [slaLevel][priority];
return addHours (createdAt, hours);
}
Agentic Task Scheduler
The scheduler reviews client data and generates proactive tasks:
Triggers
Contract renewal approaching (30/60/90 days)
Warranty expiring on assets
Regular maintenance schedules
SLA breaches imminent
Usage anomalies detected
Client inactivity (check-in needed)
Agent Workflow
1. Daily scan of all clients
2. Check triggers against current state
3. Generate tasks with appropriate priority
4. Assign to staff based on workload
5. Send notifications as needed
6. Log all actions for audit
Client Portal Features
Dashboard
Open tickets summary
Recent activity
Upcoming scheduled maintenance
Invoice status
Quick links
Ticket Management
Submit new ticket
View ticket history
Add comments/attachments
Track SLA status
Billing
View invoices
Download PDFs
Payment history
Update payment method
Documentation
Knowledge base access
Network diagrams
Asset inventory
Contact list
API Routes
# Public
GET /api/contact - Contact form submission
# Auth
POST /api/auth/login - Portal login
POST /api/auth/logout - Logout
GET /api/auth/me - Current user
# Client Portal
GET /api/portal/dashboard - Dashboard data
GET /api/portal/tickets - List tickets
POST /api/portal/tickets - Create ticket
GET /api/portal/tickets/:id
POST /api/portal/tickets/:id/comments
GET /api/portal/invoices - List invoices
GET /api/portal/invoices/:id/pdf
GET /api/portal/assets - List assets
# Internal CRM (staff only)
GET /api/crm/organizations
POST /api/crm/organizations
GET /api/crm/organizations/:id
PUT /api/crm/organizations/:id
GET /api/crm/contacts
GET /api/crm/tickets
PUT /api/crm/tickets/:id
GET /api/crm/tasks
POST /api/crm/tasks
GET /api/crm/reports/sla
GET /api/crm/reports/revenue
MVP Phases
Phase 1: Marketing Site
Phase 2: Client Portal (Basic)
Phase 3: CRM Core
Phase 4: Full CRM
Phase 5: Automation