Skip to main content

Overview

VizBoard is built on Next.js 15.3.2 with the App Router, leveraging modern React 19 features including Server Components, Server Actions, and streaming. The architecture follows a clear separation between server-side data operations and client-side interactions.

Core Architecture Patterns

Next.js App Router Structure

The application uses Next.js App Router with route groups for logical organization:
src/app/
├── (auth)/              # Authentication routes (login, signup)
├── (homepage)/          # Public homepage
├── projects/            # Project management UI
│   └── [projectId]/
│       └── dashboard/   # Dashboard view
├── public/              # Public dashboard sharing
│   └── [idPublic]/
├── settings/            # User settings
├── actions/             # Server Actions
└── api/                 # API Routes
Route groups like (auth) and (homepage) allow for shared layouts without affecting the URL structure.

Server Components by Default

VizBoard maximizes the use of React Server Components (RSC) for optimal performance:
  • Pages are Server Components - All route pages default to server rendering
  • Data fetching happens on the server - Direct database queries via Prisma
  • Reduced client bundle - Only interactive components are client-side
  • Automatic code splitting - Each route loads only necessary code
src/app/projects/[projectId]/dashboard/page.tsx
export default async function DashboardPage({
  params,
}: {
  params: Promise<{ projectId: string }>
}) {
  const session = await auth()
  const { projectId } = await params
  
  // Direct database query in Server Component
  const project = await prisma.project.findUnique({
    where: { id: projectId },
    include: {
      widgets: true,
      dbconnections: true,
    },
  })
  
  return <DashboardLayout project={project} />
}

Server Actions

VizBoard uses Server Actions for all data mutations, providing type-safe server-side operations:

Authentication Actions

Located in src/app/actions/auth/
  • User registration
  • Credential validation
  • Password hashing with bcrypt

Project Actions

Located in src/app/actions/project/
  • CRUD operations
  • Connection management
  • Schema introspection
  • Validation

Dashboard Actions

Located in src/app/actions/dashboard/
  • Widget CRUD
  • Widget ordering
  • Data fetching
  • Table queries

User Actions

Located in src/app/actions/user/
  • Profile updates
  • Settings management
src/app/actions/project/crud.ts
"use server"

import { auth } from "@/lib/auth/auth"
import { encrypt } from "@/lib/crypto/crypto"
import prisma from "@/lib/db/prisma"
import { validateProject } from "@/lib/projects/schemas"
import { revalidatePath } from "next/cache"

export async function createProjectWithConnections(input) {
  // 1. Authentication check
  const session = await auth()
  if (!session?.user) {
    redirect("/login")
  }
  
  // 2. Input validation with Zod
  const validationResult = validateProject(input)
  if (!validationResult.success) {
    return { success: false, error: validationResult.error }
  }
  
  // 3. Database transaction
  const project = await prisma.$transaction(async (tx) => {
    const createdProject = await tx.project.create({
      data: { ...validatedData }
    })
    
    // Encrypt and save connections
    for (const conn of connections) {
      const encryptedAccess = encrypt(JSON.stringify(conn))
      await tx.dbConnection.create({
        data: { projectId: createdProject.id, dbAccess: encryptedAccess }
      })
    }
    
    return createdProject
  })
  
  // 4. Revalidate cached paths
  revalidatePath("/projects")
  
  return { success: true, projectId: project.id }
}

Authentication Architecture

NextAuth.js v5 Integration

VizBoard uses NextAuth.js v5 (beta) with multiple authentication strategies:
session: {
  strategy: "jwt",
  maxAge: 30 * 24 * 60 * 60, // 30 days
}
Key Features:
  • JWT-based sessions for stateless authentication
  • OAuth integration (Google, GitHub)
  • Credentials-based login with hashed passwords
  • Prisma adapter for user/account storage
  • Custom callbacks for token enrichment

Database Architecture

Dual Database Pattern

VizBoard employs a unique dual database architecture:

Application Database

Prisma + PostgreSQLStores:
  • Users & accounts
  • Projects & widgets
  • Connection metadata (encrypted)
  • Schema cache

External Databases

Knex.js + PostgreSQLConnects to:
  • User’s external PostgreSQL databases
  • Dynamic query execution
  • Schema introspection
  • Real-time data fetching

Connection Encryption

All external database credentials are encrypted using AES-256-GCM:
lib/crypto/crypto.ts
import crypto from 'crypto'

const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'base64')

export function encrypt(text: string): string {
  const iv = crypto.randomBytes(16)
  const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv)
  
  let encrypted = cipher.update(text, 'utf8', 'hex')
  encrypted += cipher.final('hex')
  const authTag = cipher.getAuthTag()
  
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`
}

Schema Introspection

VizBoard automatically introspects external database schemas using knex-schema-inspector:
  1. Connection Validation - Test connection on creation/update
  2. Schema Discovery - Extract tables, columns, types, relationships
  3. Cache Storage - Store schema JSON in dbConnection.dbSchema
  4. Timestamp Tracking - Update lastIntrospectionAt
  5. Widget Configuration - Use cached schema for UI dropdowns
const inspector = schemaInspector(knexConnection)
const tables = await inspector.tables()
const columns = await inspector.columnInfo(tableName)

await prisma.dbConnection.update({
  where: { id: connectionId },
  data: {
    dbSchema: JSON.stringify({ tables, columns }),
    lastIntrospectionAt: new Date(),
    isValid: true,
  }
})

State Management

Layered State Architecture

VizBoard uses different state management strategies for different needs:

Server State

Server Components + SWRFor:
  • Projects list
  • Widget data
  • User profile
Benefits:
  • Automatic revalidation
  • Cache management
  • Loading states

Client State

Zustand StoresFor:
  • Widget dialog forms
  • Project draft state
  • UI preferences
Benefits:
  • Lightweight
  • No boilerplate
  • DevTools support

Form State

React Hook FormFor:
  • Project forms
  • Widget configuration
  • Settings forms
Benefits:
  • Validation with Zod
  • Performance
  • Error handling

Zustand Store Examples

// src/store/dashboard/widgetDialogStore.ts
import { create } from 'zustand'

interface WidgetDialogStore {
  isOpen: boolean
  widgetType: string | null
  editingWidget: Widget | null
  openDialog: (type: string) => void
  closeDialog: () => void
}

export const useWidgetDialog = create<WidgetDialogStore>((set) => ({
  isOpen: false,
  widgetType: null,
  editingWidget: null,
  openDialog: (type) => set({ isOpen: true, widgetType: type }),
  closeDialog: () => set({ isOpen: false, editingWidget: null }),
}))

SWR Data Fetching

For client-side data fetching and caching:
hooks/projects/useProjectsWithStore.ts
import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

export function useProjects() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/userprojects',
    fetcher,
    {
      revalidateOnFocus: true,
      refreshInterval: 30000, // 30 seconds
    }
  )
  
  return {
    projects: data?.projects || [],
    isLoading,
    error,
    refresh: mutate,
  }
}

API Routes vs Server Actions

When to Use Each

  • Form submissions - Direct form action integration
  • Mutations - Create, update, delete operations
  • Authentication required - Built-in session access
  • Revalidation - Automatic cache invalidation
Example:
<form action={createProject}>
  <input name="title" />
  <button type="submit">Create</button>
</form>
  • Client-side fetching - SWR/fetch integration
  • Webhooks - External service callbacks
  • Public endpoints - Shared dashboards
  • Non-mutation queries - Read-only data
Example:
GET /api/projects/[projectId]/dashboard
GET /api/projects/[projectId]/public
PATCH /api/widgets/[widgetId]

Caching & Revalidation

VizBoard uses Next.js 15’s enhanced caching mechanisms:

Cache Strategies

// Cached until revalidated
export default async function ProjectsPage() {
  const projects = await getProjects()
  return <ProjectsList projects={projects} />
}

Security Architecture

Credential Encryption

  • AES-256-GCM encryption
  • Unique IV per entry
  • Auth tags for integrity
  • Base64-encoded keys

Authentication

  • JWT sessions (30 days)
  • bcrypt password hashing
  • OAuth with Google/GitHub
  • Server-side validation

Authorization

  • Project ownership checks
  • Session validation per action
  • Prisma cascade deletes
  • User ID verification

Input Validation

  • Zod schema validation
  • Type-safe Server Actions
  • SQL injection prevention
  • XSS protection

Performance Optimizations

Database Level

  • Connection pooling - Reuse database connections
  • Transactions - Atomic operations with prisma.$transaction
  • Select optimization - Only fetch required fields
  • Parallel introspection - Promise.allSettled for multiple connections

Application Level

  • Code splitting - Route-based automatic splitting
  • Server Components - Zero client-side JavaScript for static content
  • Streaming - Progressive rendering with Suspense
  • Image optimization - Next.js automatic image optimization

Client Level

  • SWR caching - Reduce redundant API calls
  • Debouncing - Form inputs and search
  • Lazy loading - Widget components loaded on demand
  • Optimistic updates - Instant UI feedback
The combination of Server Components, Server Actions, and aggressive caching results in fast page loads and instant interactions.

Build docs developers (and LLMs) love