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

ComponentTechnologyRationale
FrameworkNext.js 14+ (App Router)Full-stack, SEO, performance
StylingTailwind CSSRapid development, professional theme
Componentsshadcn/uiPolished, accessible components
ChartsRecharts or Chart.jsDashboard visualizations
FormsReact Hook Form + ZodValidation, UX

Backend

ComponentTechnologyRationale
APINext.js API RoutesUnified codebase
DatabasePostgreSQLRelational, robust
ORMPrismaType-safe, migrations
AuthClerk or NextAuth.jsMulti-tenant portal auth
EmailResendTransactional emails
File StorageS3/Cloudflare R2Documents, attachments

CRM/Automation

ComponentTechnologyRationale
Task QueueBullMQ + RedisBackground jobs, scheduling
Agentic SchedulerCustom + LLM integrationSmart task prioritization
SLA EngineCustom logicEscalation, notifications
NotificationsEmail + 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

  • Homepage with hero, services, trust signals
  • Services pages (detailed)
  • About page
  • Contact form
  • Basic SEO

Phase 2: Client Portal (Basic)

  • Authentication system
  • Client dashboard
  • Ticket submission
  • Ticket history view

Phase 3: CRM Core

  • Organization management
  • Contact management
  • Basic ticket workflow
  • Staff dashboard

Phase 4: Full CRM

  • SLA engine
  • Time tracking
  • Asset management
  • Invoicing

Phase 5: Automation

  • Agentic task scheduler
  • Automated notifications
  • Reporting dashboards
  • Integration APIs