• ShipSaaSDocumentation
  • Introduction
  • Getting Started
    • Installation
    • Environment Setup
    • Database Setup
    • Development Guide
  • Database
    • Repository Pattern
    • Drizzle Queries
    • Migration Workflow
    • Drizzle Studio
    • Best Practices
  • File Management
    • File Upload Component
    • Server Actions
    • Storage Configuration
  • Authentication Methods
    • Credentials Authentication
    • Magic Link Authentication
    • Two-Factor Authentication (2FA)
    • Social Authentication
    • API Keys & Bearer Tokens
    • Organizations (Multi-tenant)
  • Authorization & Permissions
    • Roles & Hierarchy
    • Route Protection
    • API Protection
    • CASL Permissions
  • Email & Notifications
    • Resend Setup
    • React Email Templates
    • Notification System
    • Email Workflows
    • Customization & Extension
  • Stripe & Payments
    • Stripe Setup
    • Better Auth Integration
    • Checkout Methods
    • Subscription Management
    • Admin Dashboard
    • Webhook Events
  • Architecture & Development
    • Presentation Layer
    • Service Layer
    • Persistence Layer
    • Helpers Architecture
    • Project Structure
    • Development Workflow
  • Testing
    • General Testing
    • Unit Testing
    • UI Component Testing
    • Repository Testing
    • Service Testing
    • End-to-End Testing
  • Deployment
    • Vercel Deployment
    • GitHub Actions CI/CD
    • Database Backup & Restore
    • Database Migrations
    • Troubleshooting Deployment
  • AI Agents Integration
  • Blog Management
    • Blog Setup
    • MDX Blog
    • Database Blog
  • Async Tasks
    • Setup
    • Email Workflows
    • Payment Processing
    • Maintenance Jobs
  1. Documentation
  2. Architecture & Development
  3. Helpers Architecture
Buydocschangelogsdemocommunity

Helpers Architecture

PreviousPersistence LayerNextProject Structure
On this page
Helper Architecture OverviewClient Layer HelpersService Layer HelpersDatabase Layer HelpersMigration StrategyBest Practices Summary

The boilerplate enforces strict separation between client and server helpers based on file location. This architectural pattern ensures proper execution context, prevents runtime errors, and maintains clean boundaries between browser and server code.

Critical Rule: Helper location determines execution context. Client helpers (/lib/helper/) cannot access server resources, while server helpers (/services/helpers/, /db/helpers/) cannot run in the browser.

Perfect for: Maintaining clear client/server boundaries, preventing execution context errors, and organizing utility functions with proper separation of concerns.

Helper Architecture Overview

The architecture separates helpers into three distinct layers based on execution context and responsibilities:

Three-Layer Helper System:

🔵 CLIENT LAYER (/lib/helper/)
├── Pure client-side functions
├── Browser-compatible utilities
├── UI formatting and validation
└── No server resource access
            ⬇️
🟠 SERVICE LAYER (/services/helpers/)
├── Server-side business logic
├── Environment variable access
├── External API integrations
└── Complex server operations
            ⬇️
🟢 DATABASE LAYER (/db/helpers/)
├── Database query utilities
├── Data transformation helpers
├── ORM-specific operations
└── Migration assistance

Key Principles:

  • Execution Context Isolation - Each layer runs in its intended environment
  • Dependency Direction - Higher layers can use lower layers, not vice versa
  • Resource Access Control - Access restricted by layer boundaries
  • Type Safety - Proper TypeScript boundaries between layers

Allowed Dependencies by Layer:

🔵 Client Layer (/lib/helper/):

✅ CAN USE:
• Browser APIs (localStorage, navigator, etc.)
• Pure JavaScript functions
• Client-side libraries
• Other client helpers
• Shared types and constants

❌ CANNOT USE:
• Environment variables (process.env)
• Database connections
• File system operations
• External API calls
• Server-only libraries

🟠 Service Layer (/services/helpers/):

✅ CAN USE:
• Environment variables
• External API calls
• File system operations
• Client helpers (as parameters)
• Database helpers
• Server-only libraries

❌ CANNOT USE:
• Browser APIs directly
• Client-only libraries

🟢 Database Layer (/db/helpers/):

✅ CAN USE:
• Database connections
• ORM operations
• File system for migrations
• Environment variables
• Client helpers (as parameters)

❌ CANNOT USE:
• Browser APIs
• Business logic
• External API calls (except DB-related)

Recommended Data Flow:

typescript
// ✅ Good: Parameters flow down, results flow up

// 1. CLIENT HELPER (Pure function)
export function getReferenceIdByBillingMode(
  billingMode: BillingModes,
  userId: string,
  organizationId?: string
): string {
  return billingMode === BillingModes.ORGANIZATION
    ? organizationId || userId
    : userId
}

// 2. SERVICE HELPER (Uses client helper + server resources)
export function getCurrentReferenceId(
  userId: string,
  organizationId?: string
): string {
  const billingMode = env.NEXT_PUBLIC_BILLING_MODE // Server resource
  return getReferenceIdByBillingMode(billingMode, userId, organizationId)
}

// 3. DATABASE HELPER (Uses service helper for queries)
export async function findUserSubscription(
  userId: string,
  organizationId?: string
): Promise<Subscription | null> {
  const referenceId = getCurrentReferenceId(userId, organizationId)
  return await db.query.subscriptions.findFirst({
    where: eq(subscriptions.referenceId, referenceId)
  })
}

Data Flow Benefits:

  • Reusability - Client helpers used in multiple contexts
  • Testability - Pure functions easy to test
  • Maintainability - Clear dependency chain
  • Performance - Efficient resource usage

Client Layer Helpers

Client helpers are pure functions that run exclusively in the browser:

Pure Client Functions:

typescript
// src/lib/helper/format-helper.ts

// ✅ Format utilities (no external dependencies)
export function formatPrice(amount: number, currency = 'EUR'): string {
  return new Intl.NumberFormat('fr-FR', {
    style: 'currency',
    currency
  }).format(amount)
}

export function formatDate(
  date: Date | string,
  locale = 'en-US',
  options: Intl.DateTimeFormatOptions = {}
): string {
  const dateObj = typeof date === 'string' ? new Date(date) : date
  return dateObj.toLocaleDateString(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    ...options
  })
}

export function formatFileSize(bytes: number): string {
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
  if (bytes === 0) return '0 Bytes'

  const i = Math.floor(Math.log(bytes) / Math.log(1024))
  return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
}

// ✅ Text utilities
export function slugify(text: string): string {
  return text
    .toString()
    .toLowerCase()
    .trim()
    .replace(/\s+/g, '-')
    .replace(/[^\w-]+/g, '')
    .replace(/--+/g, '-')
    .replace(/^-+/, '')
    .replace(/-+$/, '')
}

export function truncateText(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text
  return text.substring(0, maxLength - 3) + '...'
}

Client Validation Helpers:

typescript
// src/lib/helper/validation-helper.ts

export function validateEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return regex.test(email)
}

export function validatePassword(password: string): {
  isValid: boolean
  errors: string[]
} {
  const errors: string[] = []

  if (password.length < 8) {
    errors.push('Password must be at least 8 characters')
  }

  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain uppercase letter')
  }

  if (!/[a-z]/.test(password)) {
    errors.push('Password must contain lowercase letter')
  }

  if (!/\d/.test(password)) {
    errors.push('Password must contain a number')
  }

  return {
    isValid: errors.length === 0,
    errors
  }
}

export function validateUrl(url: string): boolean {
  try {
    new URL(url)
    return true
  } catch {
    return false
  }
}

Domain-Specific Client Helpers:

typescript
// src/lib/helper/subscription-helper.ts

import type { BillingModes } from '@/services/types/common'

// ✅ Pure business logic (no server dependencies)
export function getReferenceIdByBillingMode(
  billingMode: BillingModes,
  userId: string,
  organizationId?: string
): string {
  return billingMode === BillingModes.ORGANIZATION
    ? organizationId || userId
    : userId
}

export function calculateProrationAmount(
  currentPlanPrice: number,
  newPlanPrice: number,
  daysRemaining: number,
  totalDaysInPeriod: number
): number {
  const dailyCurrentPlanCost = currentPlanPrice / totalDaysInPeriod
  const dailyNewPlanCost = newPlanPrice / totalDaysInPeriod

  const refundAmount = dailyCurrentPlanCost * daysRemaining
  const chargeAmount = dailyNewPlanCost * daysRemaining

  return chargeAmount - refundAmount
}

export function getSubscriptionStatusBadge(status: string): {
  text: string
  variant: 'success' | 'warning' | 'error' | 'info'
} {
  switch (status.toLowerCase()) {
    case 'active':
      return { text: 'Active', variant: 'success' }
    case 'trialing':
      return { text: 'Trial', variant: 'info' }
    case 'past_due':
      return { text: 'Past Due', variant: 'warning' }
    case 'canceled':
      return { text: 'Canceled', variant: 'error' }
    case 'incomplete':
      return { text: 'Incomplete', variant: 'warning' }
    default:
      return { text: status, variant: 'info' }
  }
}

Date and Time Helpers:

typescript
// src/lib/helper/date-helper.ts

export function addDays(date: Date, days: number): Date {
  const result = new Date(date)
  result.setDate(result.getDate() + days)
  return result
}

export function getDaysUntil(futureDate: Date): number {
  const now = new Date()
  const diffTime = futureDate.getTime() - now.getTime()
  return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
}

export function isExpired(expirationDate: Date): boolean {
  return new Date() > expirationDate
}

export function getRelativeTime(date: Date): string {
  const now = new Date()
  const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)

  if (diffInSeconds < 60) return 'just now'
  if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`
  if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`
  if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`

  return date.toLocaleDateString()
}

What Client Helpers CANNOT Do:

typescript
// ❌ BAD: Environment variables in client helpers
import { env } from '@/env'
export const BILLING_MODE = env.NEXT_PUBLIC_BILLING_MODE // Server access!

// ❌ BAD: Database access in client helpers
import db from '@/db/models/db'
export async function getUser(id: string) {
  return await db.query.users.findFirst() // Database access!
}

// ❌ BAD: External API calls in client helpers
export async function fetchStripeCustomer(id: string) {
  return await stripe.customers.retrieve(id) // External API!
}

// ❌ BAD: File system operations
import fs from 'fs'
export function readConfigFile() {
  return fs.readFileSync('./config.json') // File system!
}

// ❌ BAD: Node.js-specific imports
import { createHash } from 'crypto'
export function hashPassword(password: string) {
  return createHash('sha256').update(password).digest('hex')
}

Correct Alternatives:

typescript
// ✅ GOOD: Pass server data as parameters
export function getReferenceIdByBillingMode(
  billingMode: BillingModes, // Parameter from server
  userId: string,
  organizationId?: string
): string {
  return billingMode === BillingModes.ORGANIZATION
    ? organizationId || userId
    : userId
}

// ✅ GOOD: Browser-compatible crypto
export async function hashPasswordClient(password: string): Promise<string> {
  if (typeof crypto !== 'undefined' && crypto.subtle) {
    const encoder = new TextEncoder()
    const data = encoder.encode(password)
    const hashBuffer = await crypto.subtle.digest('SHA-256', data)
    const hashArray = Array.from(new Uint8Array(hashBuffer))
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
  }
  throw new Error('Web Crypto API not available')
}

Service Layer Helpers

Service helpers contain server-side business logic and can access server resources:

Server Business Logic:

typescript
// src/services/helpers/billing-helper.ts
import { env } from '@/env'
import { getReferenceIdByBillingMode } from '@/lib/helper/subscription-helper'
import type { BillingModes } from '@/services/types/common'

// ✅ Server helper using client helper + server resources
export function getCurrentBillingMode(): BillingModes {
  return env.NEXT_PUBLIC_BILLING_MODE as BillingModes
}

export function getCurrentReferenceId(
  userId: string,
  organizationId?: string
): string {
  const billingMode = getCurrentBillingMode()
  return getReferenceIdByBillingMode(billingMode, userId, organizationId)
}

// ✅ Complex server logic
export async function calculateSubscriptionMetrics(
  subscriptions: Subscription[]
): Promise<{
  mrr: number
  arr: number
  churnRate: number
  ltv: number
}> {
  const activeSubscriptions = subscriptions.filter(s => s.status === 'active')

  // Monthly Recurring Revenue
  const mrr = activeSubscriptions.reduce((sum, sub) => {
    const monthlyAmount = sub.interval === 'year'
      ? sub.amount / 12
      : sub.amount
    return sum + monthlyAmount
  }, 0)

  // Annual Recurring Revenue
  const arr = mrr * 12

  // Churn Rate calculation (requires historical data)
  const churnRate = await calculateChurnRate(subscriptions)

  // Lifetime Value
  const averageMonthlyRevenue = mrr / activeSubscriptions.length
  const ltv = averageMonthlyRevenue / (churnRate / 100)

  return { mrr, arr, churnRate, ltv }
}

async function calculateChurnRate(subscriptions: Subscription[]): Promise<number> {
  // Implementation with database queries for historical data
  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
  // ... complex churn calculation
  return 5.2 // Example churn rate percentage
}

Email and Notification Helpers:

typescript
// src/services/helpers/email-helper.ts
import { env } from '@/env'
import { Resend } from 'resend'

const resend = new Resend(env.RESEND_API_KEY)

export interface EmailTemplate {
  to: string
  subject: string
  template: string
  data: Record<string, any>
}

export async function sendTemplatedEmail(
  emailData: EmailTemplate
): Promise<{ success: boolean; messageId?: string; error?: string }> {
  try {
    const result = await resend.emails.send({
      from: env.EMAIL_FROM_ADDRESS,
      to: emailData.to,
      subject: emailData.subject,
      html: await renderTemplate(emailData.template, emailData.data),
    })

    return {
      success: true,
      messageId: result.data?.id,
    }
  } catch (error) {
    console.error('Email send failed:', error)
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    }
  }
}

export async function sendWelcomeEmail(
  userEmail: string,
  userName: string
): Promise<void> {
  await sendTemplatedEmail({
    to: userEmail,
    subject: 'Welcome to Our Platform!',
    template: 'welcome',
    data: { userName }
  })
}

export async function sendSubscriptionConfirmation(
  userEmail: string,
  subscription: Subscription
): Promise<void> {
  await sendTemplatedEmail({
    to: userEmail,
    subject: 'Subscription Confirmed',
    template: 'subscription-confirmation',
    data: { subscription }
  })
}

async function renderTemplate(
  templateName: string,
  data: Record<string, any>
): Promise<string> {
  // Template rendering logic
  const template = await loadEmailTemplate(templateName)
  return template.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match)
}

External API Integrations:

typescript
// src/services/helpers/stripe-helper.ts
import Stripe from 'stripe'
import { env } from '@/env'

const stripe = new Stripe(env.STRIPE_SECRET_KEY)

export async function createStripeCustomer(
  email: string,
  name?: string
): Promise<Stripe.Customer> {
  return await stripe.customers.create({
    email,
    name,
    metadata: {
      source: 'webapp',
      created_at: new Date().toISOString(),
    },
  })
}

export async function createStripeSubscription(
  customerId: string,
  priceId: string,
  options: {
    trialDays?: number
    couponId?: string
    quantity?: number
  } = {}
): Promise<Stripe.Subscription> {
  const subscriptionData: Stripe.SubscriptionCreateParams = {
    customer: customerId,
    items: [{
      price: priceId,
      quantity: options.quantity || 1,
    }],
    metadata: {
      source: 'webapp',
    },
  }

  if (options.trialDays) {
    subscriptionData.trial_period_days = options.trialDays
  }

  if (options.couponId) {
    subscriptionData.coupon = options.couponId
  }

  return await stripe.subscriptions.create(subscriptionData)
}

export async function handleWebhookEvent(
  event: Stripe.Event
): Promise<void> {
  console.log(`Processing Stripe event: ${event.type}`)

  switch (event.type) {
    case 'customer.subscription.created':
      await handleSubscriptionCreated(event.data.object as Stripe.Subscription)
      break
    case 'customer.subscription.updated':
      await handleSubscriptionUpdated(event.data.object as Stripe.Subscription)
      break
    case 'invoice.payment_succeeded':
      await handlePaymentSucceeded(event.data.object as Stripe.Invoice)
      break
    default:
      console.log(`Unhandled event type: ${event.type}`)
  }
}

File Processing Helpers:

typescript
// src/services/helpers/file-helper.ts
import fs from 'fs/promises'
import path from 'path'
import { env } from '@/env'

export async function saveUploadedFile(
  file: File,
  directory: string
): Promise<{
  filename: string
  path: string
  size: number
}> {
  const uploadDir = path.join(env.UPLOAD_PATH, directory)
  await fs.mkdir(uploadDir, { recursive: true })

  const filename = `${Date.now()}-${file.name}`
  const filePath = path.join(uploadDir, filename)

  const buffer = Buffer.from(await file.arrayBuffer())
  await fs.writeFile(filePath, buffer)

  return {
    filename,
    path: filePath,
    size: buffer.length,
  }
}

export async function deleteFile(filePath: string): Promise<void> {
  try {
    await fs.unlink(filePath)
  } catch (error) {
    console.error('Failed to delete file:', error)
  }
}

export async function getFileMetadata(filePath: string): Promise<{
  exists: boolean
  size?: number
  modified?: Date
}> {
  try {
    const stats = await fs.stat(filePath)
    return {
      exists: true,
      size: stats.size,
      modified: stats.mtime,
    }
  } catch {
    return { exists: false }
  }
}

Advanced Server Validation:

typescript
// src/services/helpers/validation-helper.ts
import { z } from 'zod'
import { getUserByEmailDao } from '@/db/repositories/user-repository'
import { getOrganizationBySlugDao } from '@/db/repositories/organization-repository'

// ✅ Server-side validation with database checks
export const createUniqueEmailSchema = () => z.string()
  .email('Invalid email format')
  .toLowerCase()
  .refine(async (email) => {
    const existingUser = await getUserByEmailDao(email)
    return !existingUser
  }, 'Email already exists')

export const createUniqueOrganizationSlugSchema = () => z.string()
  .min(3, 'Slug must be at least 3 characters')
  .max(50, 'Slug must be less than 50 characters')
  .regex(/^[a-z0-9-]+$/, 'Slug can only contain lowercase letters, numbers, and dashes')
  .refine(async (slug) => {
    const existingOrg = await getOrganizationBySlugDao(slug)
    return !existingOrg
  }, 'Organization slug already exists')

// ✅ Complex business rules validation
export function validateSubscriptionUpgrade(
  currentPlan: Plan,
  newPlan: Plan,
  user: User
): {
  isValid: boolean
  errors: string[]
  warnings: string[]
} {
  const errors: string[] = []
  const warnings: string[] = []

  // Business rule: Cannot downgrade during trial
  if (currentPlan.isTrial && newPlan.price < currentPlan.price) {
    errors.push('Cannot downgrade during trial period')
  }

  // Business rule: Seat limits
  if (newPlan.maxSeats < user.organization?.memberCount) {
    errors.push(`New plan supports ${newPlan.maxSeats} seats but organization has ${user.organization.memberCount} members`)
  }

  // Warning: Significant price increase
  if (newPlan.price > currentPlan.price * 2) {
    warnings.push('This upgrade will more than double your monthly cost')
  }

  return {
    isValid: errors.length === 0,
    errors,
    warnings
  }
}

// ✅ Rate limiting validation
const requestCounts = new Map<string, { count: number; resetTime: number }>()

export function validateRateLimit(
  identifier: string,
  maxRequests: number = 100,
  windowMs: number = 60000 // 1 minute
): {
  allowed: boolean
  remaining: number
  resetTime: number
} {
  const now = Date.now()
  const windowStart = now - windowMs

  let userRequests = requestCounts.get(identifier)

  if (!userRequests || userRequests.resetTime < windowStart) {
    userRequests = { count: 0, resetTime: now + windowMs }
    requestCounts.set(identifier, userRequests)
  }

  userRequests.count++

  return {
    allowed: userRequests.count <= maxRequests,
    remaining: Math.max(0, maxRequests - userRequests.count),
    resetTime: userRequests.resetTime
  }
}

Database Layer Helpers

Database helpers focus on query optimization, data transformation, and ORM utilities:

Common Query Patterns:

typescript
// src/db/helpers/query-helper.ts
import { sql, and, or, eq, gte, lte, desc, asc } from 'drizzle-orm'
import db from '../models/db'

// ✅ Reusable query builders
export function buildDateRangeFilter(
  column: any,
  startDate?: Date,
  endDate?: Date
) {
  const conditions = []

  if (startDate) {
    conditions.push(gte(column, startDate))
  }

  if (endDate) {
    conditions.push(lte(column, endDate))
  }

  return conditions.length > 0 ? and(...conditions) : undefined
}

export function buildSearchFilter(
  columns: any[],
  searchTerm?: string
) {
  if (!searchTerm) return undefined

  const likePattern = `%${searchTerm.toLowerCase()}%`
  const conditions = columns.map(column =>
    sql`LOWER(${column}) LIKE ${likePattern}`
  )

  return or(...conditions)
}

export function buildSortOrder(
  sortField?: string,
  sortDirection: 'asc' | 'desc' = 'desc'
) {
  if (!sortField) return undefined

  return sortDirection === 'asc' ? asc(sortField) : desc(sortField)
}

// ✅ Generic pagination helper
export interface QueryOptions {
  limit?: number
  offset?: number
  sortField?: string
  sortDirection?: 'asc' | 'desc'
  searchTerm?: string
  dateRange?: {
    startDate?: Date
    endDate?: Date
    column?: any
  }
}

export async function executePagedQuery<T>(
  baseQuery: any,
  countQuery: any,
  options: QueryOptions = {}
): Promise<{
  data: T[]
  totalCount: number
  hasMore: boolean
}> {
  const limit = Math.min(options.limit || 20, 100)
  const offset = options.offset || 0

  // Build filters
  const conditions = []

  if (options.searchTerm && options.searchTerm.length > 0) {
    // Search conditions would be added here based on specific table
  }

  if (options.dateRange?.startDate || options.dateRange?.endDate) {
    const dateFilter = buildDateRangeFilter(
      options.dateRange.column,
      options.dateRange.startDate,
      options.dateRange.endDate
    )
    if (dateFilter) conditions.push(dateFilter)
  }

  const whereClause = conditions.length > 0 ? and(...conditions) : undefined

  // Execute queries in parallel
  const [data, countResult] = await Promise.all([
    baseQuery
      .where(whereClause)
      .limit(limit)
      .offset(offset)
      .execute(),
    countQuery
      .where(whereClause)
      .execute()
  ])

  const totalCount = countResult[0]?.count || 0

  return {
    data,
    totalCount,
    hasMore: offset + data.length < totalCount
  }
}

Advanced Query Utilities:

typescript
// ✅ Bulk operations
export async function bulkInsert<T>(
  table: any,
  records: T[],
  chunkSize: number = 1000
): Promise<void> {
  for (let i = 0; i < records.length; i += chunkSize) {
    const chunk = records.slice(i, i + chunkSize)
    await db.insert(table).values(chunk)
  }
}

export async function bulkUpdate<T>(
  table: any,
  updates: Array<{ id: string; data: Partial<T> }>,
  chunkSize: number = 1000
): Promise<void> {
  for (let i = 0; i < updates.length; i += chunkSize) {
    const chunk = updates.slice(i, i + chunkSize)

    await db.transaction(async (tx) => {
      for (const update of chunk) {
        await tx
          .update(table)
          .set(update.data)
          .where(eq(table.id, update.id))
      }
    })
  }
}

// ✅ Aggregation helpers
export async function getTableStats(
  table: any,
  groupByColumn?: any
): Promise<Array<{
  group?: string
  count: number
  createdThisMonth: number
  createdThisWeek: number
}>> {
  const now = new Date()
  const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
  const startOfWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)

  return await db
    .select({
      group: groupByColumn,
      count: sql<number>`COUNT(*)`,
      createdThisMonth: sql<number>`COUNT(CASE WHEN created_at >= ${startOfMonth} THEN 1 END)`,
      createdThisWeek: sql<number>`COUNT(CASE WHEN created_at >= ${startOfWeek} THEN 1 END)`,
    })
    .from(table)
    .groupBy(groupByColumn)
}

Type-Safe Transformations:

typescript
// src/db/helpers/transform-helper.ts
import type { UserModel, SubscriptionModel } from '../types/models'
import type { User, Subscription } from '@/services/types/domain'

// ✅ Model to DTO transformations
export function transformUserModelToDTO(userModel: UserModel): User {
  return {
    id: userModel.id,
    name: userModel.name,
    email: userModel.email,
    role: userModel.role,
    isActive: userModel.isActive,
    createdAt: userModel.createdAt,
    updatedAt: userModel.updatedAt,

    // Computed fields
    displayName: userModel.name || userModel.email.split('@')[0],
    avatarUrl: userModel.image || generateGravatarUrl(userModel.email),

    // Parse JSON fields
    metadata: parseJsonField(userModel.metadata),
    preferences: parseJsonField(userModel.preferences),
  }
}

export function transformSubscriptionModelToDTO(
  subModel: SubscriptionModel
): Subscription {
  return {
    id: subModel.id,
    userId: subModel.userId,
    planId: subModel.planId,
    status: subModel.status,
    amount: subModel.amount,
    currency: subModel.currency,
    interval: subModel.interval,
    createdAt: subModel.createdAt,
    updatedAt: subModel.updatedAt,

    // Computed fields
    isActive: subModel.status === 'active',
    nextBillingDate: calculateNextBillingDate(subModel),
    daysUntilRenewal: calculateDaysUntilRenewal(subModel),
  }
}

// ✅ Batch transformations
export function transformUsersToDTO(userModels: UserModel[]): User[] {
  return userModels.map(transformUserModelToDTO)
}

export function transformSubscriptionsToDTO(
  subModels: SubscriptionModel[]
): Subscription[] {
  return subModels.map(transformSubscriptionModelToDTO)
}

// ✅ Utility functions
function parseJsonField(jsonString: string | null): any {
  if (!jsonString) return null

  try {
    return JSON.parse(jsonString)
  } catch (error) {
    console.warn('Failed to parse JSON field:', error)
    return null
  }
}

function generateGravatarUrl(email: string, size: number = 200): string {
  // Using client helper for hash calculation (passed as parameter)
  const hash = require('crypto')
    .createHash('md5')
    .update(email.toLowerCase().trim())
    .digest('hex')

  return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`
}

function calculateNextBillingDate(sub: SubscriptionModel): Date | null {
  if (sub.status !== 'active' || !sub.currentPeriodEnd) return null
  return new Date(sub.currentPeriodEnd)
}

function calculateDaysUntilRenewal(sub: SubscriptionModel): number | null {
  const nextBilling = calculateNextBillingDate(sub)
  if (!nextBilling) return null

  const now = new Date()
  const diffTime = nextBilling.getTime() - now.getTime()
  return Math.ceil(diffTime / (1000 * 60 * 60 * 24))
}

Complex Data Processing:

typescript
// ✅ Analytics and reporting helpers
export async function generateUserActivityReport(
  userId: string,
  dateRange: { startDate: Date; endDate: Date }
): Promise<{
  loginCount: number
  subscriptionEvents: number
  supportTickets: number
  revenueGenerated: number
}> {
  const { startDate, endDate } = dateRange

  // Parallel queries for different metrics
  const [loginEvents, subscriptionEvents, supportTickets, revenue] = await Promise.all([
    // Count login events
    db.select({ count: sql<number>`COUNT(*)` })
      .from(auditLogs)
      .where(and(
        eq(auditLogs.userId, userId),
        eq(auditLogs.action, 'login'),
        gte(auditLogs.createdAt, startDate),
        lte(auditLogs.createdAt, endDate)
      )),

    // Count subscription events
    db.select({ count: sql<number>`COUNT(*)` })
      .from(auditLogs)
      .where(and(
        eq(auditLogs.userId, userId),
        sql`${auditLogs.action} LIKE 'subscription_%'`,
        gte(auditLogs.createdAt, startDate),
        lte(auditLogs.createdAt, endDate)
      )),

    // Count support tickets
    db.select({ count: sql<number>`COUNT(*)` })
      .from(supportTickets)
      .where(and(
        eq(supportTickets.userId, userId),
        gte(supportTickets.createdAt, startDate),
        lte(supportTickets.createdAt, endDate)
      )),

    // Calculate revenue
    db.select({ total: sql<number>`COALESCE(SUM(${payments.amount}), 0)` })
      .from(payments)
      .where(and(
        eq(payments.userId, userId),
        eq(payments.status, 'succeeded'),
        gte(payments.createdAt, startDate),
        lte(payments.createdAt, endDate)
      ))
  ])

  return {
    loginCount: loginEvents[0]?.count || 0,
    subscriptionEvents: subscriptionEvents[0]?.count || 0,
    supportTickets: supportTickets[0]?.count || 0,
    revenueGenerated: revenue[0]?.total || 0,
  }
}

Migration Utilities:

typescript
// src/db/helpers/migration-helper.ts
import { sql } from 'drizzle-orm'
import db from '../models/db'

// ✅ Safe migration utilities
export async function columnExists(
  tableName: string,
  columnName: string
): Promise<boolean> {
  const result = await db.execute(sql`
    SELECT column_name
    FROM information_schema.columns
    WHERE table_name = ${tableName}
    AND column_name = ${columnName}
  `)

  return result.length > 0
}

export async function indexExists(indexName: string): Promise<boolean> {
  const result = await db.execute(sql`
    SELECT indexname
    FROM pg_indexes
    WHERE indexname = ${indexName}
  `)

  return result.length > 0
}

export async function addColumnIfNotExists(
  tableName: string,
  columnName: string,
  columnDefinition: string
): Promise<void> {
  const exists = await columnExists(tableName, columnName)

  if (!exists) {
    console.log(`Adding column ${columnName} to ${tableName}`)
    await db.execute(sql.raw(`
      ALTER TABLE "${tableName}"
      ADD COLUMN "${columnName}" ${columnDefinition}
    `))
  } else {
    console.log(`Column ${columnName} already exists in ${tableName}`)
  }
}

export async function createIndexIfNotExists(
  indexName: string,
  tableName: string,
  columns: string[],
  options: {
    unique?: boolean
    concurrent?: boolean
    where?: string
  } = {}
): Promise<void> {
  const exists = await indexExists(indexName)

  if (!exists) {
    const uniqueClause = options.unique ? 'UNIQUE' : ''
    const concurrentClause = options.concurrent ? 'CONCURRENTLY' : ''
    const whereClause = options.where ? `WHERE ${options.where}` : ''
    const columnList = columns.map(col => `"${col}"`).join(', ')

    console.log(`Creating index ${indexName}`)
    await db.execute(sql.raw(`
      CREATE ${uniqueClause} INDEX ${concurrentClause} "${indexName}"
      ON "${tableName}" (${columnList})
      ${whereClause}
    `))
  } else {
    console.log(`Index ${indexName} already exists`)
  }
}

// ✅ Data migration helpers
export async function migrateDataInBatches<T>(
  selectQuery: string,
  processFunction: (batch: T[]) => Promise<void>,
  batchSize: number = 1000
): Promise<void> {
  let offset = 0
  let totalProcessed = 0

  console.log('Starting data migration...')

  while (true) {
    const batch = await db.execute(sql.raw(`
      ${selectQuery}
      LIMIT ${batchSize} OFFSET ${offset}
    `)) as T[]

    if (batch.length === 0) {
      break
    }

    await processFunction(batch)

    totalProcessed += batch.length
    offset += batchSize

    console.log(`Processed ${totalProcessed} records...`)

    // Small delay to avoid overwhelming the database
    await new Promise(resolve => setTimeout(resolve, 100))
  }

  console.log(`Data migration completed. Total records processed: ${totalProcessed}`)
}

// ✅ Backup helpers
export async function backupTable(
  tableName: string,
  backupSuffix: string = new Date().toISOString().split('T')[0]
): Promise<void> {
  const backupTableName = `${tableName}_backup_${backupSuffix}`

  console.log(`Creating backup table: ${backupTableName}`)
  await db.execute(sql.raw(`
    CREATE TABLE "${backupTableName}" AS
    SELECT * FROM "${tableName}"
  `))
}

Migration Strategy

How to migrate existing helpers to the proper architecture:

Step-by-Step Migration:

1. Analyze Current Helpers:

bash
# Find all helper files
find src -name "*helper*" -type f

# Analyze dependencies
grep -r "import.*env" src/lib/helper/
grep -r "process.env" src/lib/helper/
grep -r "import.*db" src/lib/helper/

2. Categorize by Usage:

typescript
// Before Migration Analysis
// src/lib/helper/subscription-helper.ts - PROBLEMATIC

import { env } from '@/env' // ❌ Server dependency in client helper

export const BILLING_MODE = env.NEXT_PUBLIC_BILLING_MODE // ❌ Server access

export function getReferenceIdByBillingMode(
  userId?: string,
  organizationId?: string
): string | undefined {
  // ✅ This logic is actually pure client logic
  return BILLING_MODE === BillingModes.ORGANIZATION
    ? organizationId || userId
    : userId
}

3. Split the Helper:

typescript
// ✅ After Migration - Client Helper
// src/lib/helper/subscription-helper.ts
export function getReferenceIdByBillingMode(
  billingMode: BillingModes, // Now passed as parameter
  userId: string,
  organizationId?: string
): string {
  return billingMode === BillingModes.ORGANIZATION
    ? organizationId || userId
    : userId
}

// ✅ After Migration - Service Helper
// src/services/helpers/billing-helper.ts
import { env } from '@/env'
import { getReferenceIdByBillingMode } from '@/lib/helper/subscription-helper'

export function getCurrentBillingMode(): BillingModes {
  return env.NEXT_PUBLIC_BILLING_MODE as BillingModes
}

export function getCurrentReferenceId(
  userId: string,
  organizationId?: string
): string {
  const billingMode = getCurrentBillingMode()
  return getReferenceIdByBillingMode(billingMode, userId, organizationId)
}

4. Update Import Statements:

typescript
// Before: All imports from problematic helper
import { getReferenceIdByBillingMode, BILLING_MODE } from '@/lib/helper/subscription-helper'

// After: Specific imports from appropriate layers
// In client components:
import { getReferenceIdByBillingMode } from '@/lib/helper/subscription-helper'

// In server code:
import { getCurrentReferenceId } from '@/services/helpers/billing-helper'

Verification Tests:

typescript
// Test client helpers (should work in browser)
describe('Client Helpers', () => {
  it('should work without server dependencies', () => {
    const result = getReferenceIdByBillingMode(
      BillingModes.ORGANIZATION,
      'user-id',
      'org-id'
    )
    expect(result).toBe('org-id')
  })

  it('should fallback to user ID', () => {
    const result = getReferenceIdByBillingMode(
      BillingModes.USER,
      'user-id',
      'org-id'
    )
    expect(result).toBe('user-id')
  })
})

// Test server helpers (mock environment)
describe('Server Helpers', () => {
  beforeEach(() => {
    vi.mock('@/env', () => ({
      env: { NEXT_PUBLIC_BILLING_MODE: 'organization' }
    }))
  })

  it('should get current reference ID', () => {
    const result = getCurrentReferenceId('user-id', 'org-id')
    expect(result).toBe('org-id')
  })
})

// Integration test
describe('Helper Integration', () => {
  it('should work together correctly', () => {
    const billingMode = getCurrentBillingMode()
    const referenceId = getReferenceIdByBillingMode(
      billingMode,
      'user-id',
      'org-id'
    )
    expect(referenceId).toBe('org-id')
  })
})

Bundle Analysis:

typescript
// Check that client helpers don't include server code
// This should not appear in client bundle:
import { env } from '@/env'
import db from '@/db'

// Tools like webpack-bundle-analyzer can verify:
// 1. Client helpers are included in client bundle
// 2. Server helpers are NOT included in client bundle
// 3. No server dependencies leak into client code

Migration Pitfalls:

1. Incomplete Separation:

typescript
// ❌ Still accessing server resources in client helper
export function getConfig() {
  if (typeof window === 'undefined') {
    return process.env.CONFIG // Still server access!
  }
  return window.localStorage.getItem('config')
}

// ✅ Proper separation
// Client helper
export function getClientConfig(): string | null {
  return localStorage.getItem('config')
}

// Server helper
export function getServerConfig(): string {
  return process.env.CONFIG
}

2. Wrong Layer Choice:

typescript
// ❌ Database logic in service helper
// src/services/helpers/user-helper.ts
export async function getUserDisplayName(id: string): Promise<string> {
  const user = await db.query.users.findFirst({ // Wrong layer!
    where: eq(users.id, id)
  })
  return user?.name || 'Unknown'
}

// ✅ Correct layering
// Database helper
export async function getUserById(id: string): Promise<User | null> {
  return await db.query.users.findFirst({
    where: eq(users.id, id)
  })
}

// Service helper
export async function getUserDisplayName(id: string): Promise<string> {
  const user = await getUserById(id) // Use DB helper
  return user?.name || 'Unknown'
}

3. Circular Dependencies:

typescript
// ❌ Circular dependency
// client-helper.ts
import { serverFunction } from '@/services/helpers/server-helper'

// server-helper.ts
import { clientFunction } from '@/lib/helper/client-helper' // Circular!

// ✅ Proper dependency direction
// Only higher layers import from lower layers
// Services can import client helpers
// Client helpers cannot import service helpers

4. Runtime Environment Errors:

typescript
// ❌ Will break in browser
export function hashPassword(password: string) {
  const crypto = require('crypto') // Node.js only!
  return crypto.createHash('sha256').update(password).digest('hex')
}

// ✅ Environment-appropriate solution
// Server helper
export function hashPasswordServer(password: string): string {
  const crypto = require('crypto')
  return crypto.createHash('sha256').update(password).digest('hex')
}

// Client helper (if needed)
export async function hashPasswordClient(password: string): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(password)
  const hashBuffer = await crypto.subtle.digest('SHA-256', data)
  const hashArray = Array.from(new Uint8Array(hashBuffer))
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}

Best Practices Summary

🔵

Client Helper Rules

• Pure functions only - No side effects
• Browser-compatible - No Node.js APIs
• Parameter-based - No environment access
• Testable - Easy to unit test
• Reusable - Can be called from server
🟠

Service Helper Rules

• Server resources OK - Environment, APIs, files
• Business logic - Complex operations
• Can use client helpers - Pass data as parameters
• External integrations - Third-party APIs
• Validation - Server-side rules
🟢

Database Helper Rules

• Query optimization - Efficient database access
• Data transformation - Type conversions
• Migration utilities - Schema evolution
• Batch operations - Performance optimization
• No business logic - Data operations only

Helper architecture ready! Your utilities are properly separated by execution context, preventing runtime errors and maintaining clean boundaries between client and server code.