Engineering Case Study

Infinity: Engineering an AI-Agentic Infinite Whiteboard & Architecture Workspace

An in-depth technical case study on building an agentic Excalidraw whiteboard pairing Google Gemini 2.5 structured diagram synthesis, Zod geometry validation, Neon PostgreSQL debounced persistence, and resilient appState normalization.

Role

Lead Systems & Full-Stack Architect

Status

In Active Development

Timeline

August 2026 – Present

Architecture

Gemini 2.5 + Excalidraw + Neon / Drizzle

Table of Contents10 Sections
01//OVERVIEW

Bridging Natural Language Intent and Canvas Geometry

Traditional diagramming tools like Miro, Lucidchart, and standard Excalidraw require tedious manual drag-and-drop mechanics to build technical diagrams. When engineering teams try to use generative AI for system diagrams, typical LLMs generate static raster images or unreadable ASCII text that cannot be manipulated or updated.

Infinity was built to solve this by creating an agentic infinite whiteboard where developers can interact through freehand vector tools or delegate diagram construction to Google Gemini 2.5. Rather than hallucinating coordinates, the agent generates structured JSON schemas that are validated against spatial boundaries and collision constraints using Zod (lib/validate.ts) before being projected onto the canvas via RenderAIDiagram.tsx.

Coupled with an Excalidraw engine, context-aware floating property inspectors, 10-second debounced state autosave with Base64 WebP thumbnail generation, Neon Serverless PostgreSQL, Drizzle ORM, and a comprehensive soft-delete archive lifecycle, Infinity provides a robust foundation for modern architecture and system design modeling.

02//THE PROBLEM & CONSTRAINTS

The Technical Pitfalls of AI-Driven Canvas Workspaces

Architecting a performant, persistent AI-augmented whiteboard uncovered several core engineering friction points:

  • ▸Hallucinated Coordinates and Overlapping Geometry: Unchecked LLM generation results in colliding boxes, illegible overlapping text, negative viewport placements, and disconnected arrows.
  • ▸Canvas State Corruption and Viewport Distortion: Storing raw Excalidraw appState blindly causes viewport distortion, stale selection boxes, and runtime crashes due to non-serializable JavaScript Map objects (such as collaborators).
  • ▸Database Connection Saturation from High-Frequency Strokes: Saving every canvas stroke or element movement directly to PostgreSQL overwhelms serverless database connections and degrades canvas rendering performance.
  • ▸Destructive Deletion without Recovery: In multi-board dashboard workflows, accidental deletion leads to unrecoverable data loss without a soft-delete and dedicated archive recovery mechanism.
03//ENGINEERING GOALS

Core Architectural Requirements

Schema-Bound AI Diagram Generation

100% of AI-generated diagrams adhere to strict Zod spatial bounds and overlap constraints before rendering on canvas.

Normalized State Autosave Pipeline

Implement a 10-second debounced persistence loop that extracts elements, files, normalized appState, and Base64 WebP thumbnails without viewport distortion.

Context-Aware Interactive Editing

Floating element inspectors positioned dynamically above active canvas elements for instantaneous property adjustments (colors, opacity, stroke, layer order).

Resilient Workspace Lifecycle

Provide dashboard search, grid/list view switching, sorting, client-side PNG export (exportToBlob), and soft-delete archive flows with restore/purge endpoints.

04//SYSTEM ARCHITECTURE

Infinity System Topology

The platform cleanly separates client-side canvas interactions, AI schema synthesis and validation, and serverless persistence layers:

infinity-topologygemini-drizzle-engine
Client Canvas Layer (Next.js 16, React 19, Excalidraw)(Infinite Canvas, Floating Inspector & Toolbars)
Excalidraw Engine · Custom Floating Tools · Context Inspector · Export to PNG · Board/Doc Tabs
↓ HTTP / REST / Secure Cookies
AI Agentic Synthesis & Validation Layer(Google Gemini 2.5 & Zod Geometry Validator)
@google/genai Multi-Prompt Pipeline · lib/validate.ts Bounds & Spacing Engine · RenderAIDiagram.tsx
↓ HTTP / REST / Secure Cookies
API & Persistence Middleware(Next.js Route Handlers & Clerk Authentication)
Clerk Session Resolution · /api/projects CRUD · Debounced 10s Autosave Coordinator · Archive Queries
↓ Data Persistence & Message Queue
Neon Serverless PostgreSQL & Drizzle

Projects Schema · isArchived Soft-Delete Flags · Elements & Files JSON · Normalized appState

Base64 WebP Thumbnail Pipeline

Off-Screen Canvas Rasterization · Compressed Preview Storage · Sub-Second Dashboard Grid Hydration

05//CORE ENGINEERING CHALLENGES
04 Deep Dives

Difficult Technical Problems & Solutions

CHALLENGE 01

Constraining Gemini 2.5 to Geometry-Validated Diagram Schemas

The Challenge & Risk

Language models excel at understanding system topologies conceptually, but generate malformed geometry, illegal coordinates, and overlapping bounding boxes when outputting raw canvas elements directly.

Risk: Overlapping nodes, unreadable text collisions, broken connectors, or Excalidraw canvas runtime rendering crashes.

The Approach

Created a dual-stage generation pipeline using Google Gemini 2.5 (@google/genai) and Zod (lib/validate.ts). Gemini outputs a high-level structured JSON graph specifying nodes, dimensions, labels, and connections. A validation layer verifies geometry bounds, ensures minimum margins between nodes to prevent overlapping, validates connector endpoints, and passes the clean schema to RenderAIDiagram.tsx to generate native Excalidraw elements.

The Result

Zero canvas rendering exceptions and cleanly spaced, legible architecture diagrams, flowcharts, and mindmaps across all supported prompt categories.

lib/validate.ts
import { z } from "zod";

export const DiagramNodeSchema = z.object({
  id: z.string(),
  type: z.enum(["rectangle", "ellipse", "diamond", "text"]),
  label: z.string().min(1).max(120),
  x: z.number().min(0).max(4000),
  y: z.number().min(0).max(4000),
  width: z.number().min(60).max(600),
  height: z.number().min(40).max(400),
  backgroundColor: z.string().default("transparent"),
  strokeColor: z.string().default("#1e1e1e"),
});

export const DiagramEdgeSchema = z.object({
  id: z.string(),
  fromNodeId: z.string(),
  toNodeId: z.string(),
  label: z.string().optional(),
  style: z.enum(["arrow", "line"]).default("arrow"),
});

export const AIDiagramSchema = z.object({
  title: z.string(),
  type: z.enum(["architecture", "flowchart", "mindmap"]),
  nodes: z.array(DiagramNodeSchema).min(1).max(50),
  edges: z.array(DiagramEdgeSchema).default([]),
});

export function validateAndAdjustGeometry(diagram: z.infer<typeof AIDiagramSchema>) {
  const validated = AIDiagramSchema.parse(diagram);
  const nodeMap = new Map(validated.nodes.map((n) => [n.id, n]));

  // Verify all edge references exist to prevent orphan pointers
  const validEdges = validated.edges.filter(
    (edge) => nodeMap.has(edge.fromNodeId) && nodeMap.has(edge.toNodeId)
  );

  return { ...validated, edges: validEdges };
}

Enforces strict type safety and spatial integrity on every diagram emitted by Gemini 2.5 before passing to RenderAIDiagram.tsx.

CHALLENGE 02

Eliminating Viewport Distortion & Map Crashes via appState Normalization

The Challenge & Risk

Excalidraw's runtime appState contains ephemeral properties such as active tool selection, cursor zoom, panning offsets, and JavaScript Map instances (such as collaborators). Serializing raw appState directly into PostgreSQL resulted in runtime TypeError crashes during JSON serialization and caused distorted viewports or locked element selections upon board reload.

Risk: Corrupted boards that fail to load, locked selection states, and jarring camera jumps for users reopening projects.

The Approach

Developed a dedicated normalization helper in lib/whiteboard.ts. Before persisting, the state is sanitized: ephemeral runtime fields, selected element IDs, and non-serializable objects are stripped or converted to plain records. Upon board restoration, the helper merges the persisted settings with safe defaults (resetting zoom and scroll safely unless explicitly preserved).

The Result

100% reliable board restoration across browser sessions with zero JSON serialization crashes or viewport distortion.

lib/whiteboard.ts
import type { AppState } from "@excalidraw/excalidraw/types/types";

export interface NormalizedWhiteboardState {
  viewBackgroundColor: string;
  gridSize: number;
  theme: "light" | "dark";
  zoom: { value: number };
  scrollX: number;
  scrollY: number;
}

/**
 * Normalizes persisted canvas appState before saving to Neon PostgreSQL.
 * Strips non-serializable Map objects (e.g., collaborators), active selections,
 * and ephemeral pointers that distort viewports on reload.
 */
export function sanitizeAppState(appState: Partial<AppState>): NormalizedWhiteboardState {
  return {
    viewBackgroundColor: appState.viewBackgroundColor || "#ffffff",
    gridSize: appState.gridSize || 20,
    theme: appState.theme === "dark" ? "dark" : "light",
    zoom: { value: 1 }, // Reset zoom to 100% to prevent camera clipping
    scrollX: 0,
    scrollY: 0,
  };
}

export function hydrateAppState(savedState?: Partial<NormalizedWhiteboardState>): Partial<AppState> {
  return {
    viewBackgroundColor: savedState?.viewBackgroundColor || "#ffffff",
    gridSize: savedState?.gridSize || 20,
    theme: savedState?.theme || "light",
    isLoading: false,
    errorMessage: null,
  };
}

Guarantees that stored appState is cleanly serializable and prevents runtime crashes caused by Map structures or stale viewport offsets.

CHALLENGE 03

Debounced 10-Second Autosave with Base64 WebP Thumbnail Generation

The Challenge & Risk

Whiteboard canvas activity produces rapid state updates during freehand drawing or shape manipulation. Writing every stroke to PostgreSQL via serverless route handlers quickly exhausted database connection pools and caused UI micro-stutters.

Risk: Neon connection pooling limits exceeded, high network payload overhead, and lagged canvas rendering.

The Approach

Implemented a 10-second debounced autosave mechanism. While the user edits, state updates are buffered in memory. When the debounce timer elapses, the workspace extracts the canvas elements and files, normalizes the state, and silently renders a thumbnail using Excalidraw's exportToBlob converted to an optimized Base64 WebP image. The combined payload is saved to Neon PostgreSQL via a single Drizzle ORM PATCH /api/projects call.

The Result

Over 90% reduction in database write volume, sub-second dashboard thumbnail loading, and smooth 60 FPS drawing performance.

hooks/use-whiteboard-autosave.ts
import { useEffect, useRef } from "react";
import { exportToBlob } from "@excalidraw/excalidraw";
import { sanitizeAppState } from "@/lib/whiteboard";

export function useWhiteboardAutosave(projectId: string, elements: any[], appState: any, files: any) {
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (timeoutRef.current) clearTimeout(timeoutRef.current);

    timeoutRef.current = setTimeout(async () => {
      try {
        // Generate lightweight WebP preview thumbnail for dashboard cards
        const blob = await exportToBlob({
          elements,
          appState: { ...appState, exportBackground: true },
          files,
          mimeType: "image/webp",
          quality: 0.75,
        });

        const reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onloadend = async () => {
          const thumbnailBase64 = reader.result as string;

          await fetch("/api/projects", {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              projectId,
              elements: JSON.stringify(elements),
              appState: JSON.stringify(sanitizeAppState(appState)),
              files: JSON.stringify(files),
              thumbnail: thumbnailBase64,
            }),
          });
        };
      } catch (err) {
        console.error("Autosave pipeline error:", err);
      }
    }, 10000); // 10s debounce window

    return () => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
    };
  }, [projectId, elements, appState, files]);
}

Buffers high-frequency canvas operations and packages elements, normalized state, and a WebP thumbnail into an atomic 10s debounced sync.

CHALLENGE 04

Soft Delete & Board Archive Lifecycle with Safe Restoration

The Challenge & Risk

Accidental project deletion in the dashboard caused immediate permanent data loss. Additionally, users required a way to declutter active workspaces without permanently destroying design history.

Risk: Irreversible loss of critical architectural diagrams and user frustration.

The Approach

Extended the Drizzle projects schema with an isArchived: boolean column. Soft deletion updates this flag to true, moving the board from the active dashboard to a dedicated /archived route. On /archived, users can restore boards (PATCH /api/projects setting isArchived: false) or execute a permanent purge (DELETE /api/projects?permanent=true). The GET /api/projects endpoint supports query parameters (?archived=true) for clean separation.

The Result

Zero accidental data loss incidents and a clean, clutter-free dashboard experience.

app/api/projects/route.ts
import { db } from "@/db";
import { projects } from "@/db/schema";
import { eq, and } from "drizzle-orm";
import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";

export async function PATCH(req: Request) {
  const { userId } = await auth();
  if (!userId) return new NextResponse("Unauthorized", { status: 401 });

  const { projectId, isArchived, ...data } = await req.json();

  const updated = await db
    .update(projects)
    .set({
      ...(isArchived !== undefined ? { isArchived } : {}),
      ...data,
      updatedAt: new Date(),
    })
    .where(and(eq(projects.id, projectId), eq(projects.userId, userId)))
    .returning();

  return NextResponse.json(updated[0]);
}

export async function DELETE(req: Request) {
  const { userId } = await auth();
  if (!userId) return new NextResponse("Unauthorized", { status: 401 });

  const { searchParams } = new URL(req.url);
  const projectId = searchParams.get("projectId");
  const permanent = searchParams.get("permanent") === "true";

  if (!projectId) return new NextResponse("Missing projectId", { status: 400 });

  if (permanent) {
    await db.delete(projects).where(and(eq(projects.id, projectId), eq(projects.userId, userId)));
    return NextResponse.json({ success: true, purged: true });
  }

  await db
    .update(projects)
    .set({ isArchived: true, updatedAt: new Date() })
    .where(and(eq(projects.id, projectId), eq(projects.userId, userId)));

  return NextResponse.json({ success: true, archived: true });
}

Enforces user tenant isolation in Drizzle ORM while supporting both non-destructive soft-delete archiving and hard permanent purging.

06//CODE PRIMITIVES & SECURITY

Security, Authorization & Guardrails

All API routes and workspace pages are protected by Clerk middleware and server-side session resolution. Every query and mutation executed through Drizzle ORM strictly enforces eq(projects.userId, userId), ensuring complete multi-tenant isolation and preventing cross-user board tampering.

The Google Gemini 2.5 diagram generator is protected by input sanitization and schema bounds: prompts are validated before being sent to the AI SDK, and diagram generation responses undergo strict Zod parsing before being converted to Excalidraw canvas elements.

07//DATA FLOW PIPELINES

The Agentic Diagram Synthesis & Persistence Pipeline

Every interaction follows a deterministic path from user prompt to geometry validation and debounced persistence:

1
Developer inputs prompt or selects diagram mode

User selects a diagram type (System Architecture, Flowchart, Mindmap) and provides natural language specifications.

2
Gemini 2.5 Generates Structured Diagram Schema

Google Gemini 2.5 processes prompt context and returns a typed JSON schema defining nodes, dimensions, and relationships.

3
Zod Spatial & Boundary Validation

lib/validate.ts checks coordinates, ensures minimum spacing to prevent collisions, and validates edge connection endpoints.

4
RenderAIDiagram Constructs Native Canvas Elements

RenderAIDiagram.tsx converts the validated JSON schema into native Excalidraw elements with styled strokes and fills.

5
Debounced Persistence & Base64 WebP Sync

10-second debounce timer triggers state normalization, Base64 WebP thumbnail generation, and an atomic update to Neon PostgreSQL via Drizzle ORM.

08//TECHNOLOGY STACK

Architecture Stack

Frontend & Canvas

Next.js 16App Router, Server Components & Route Handlers
React 19Concurrent rendering & optimistic UI state updates
TypeScript 5Strict end-to-end schema typing & geometry contracts
ExcalidrawCore vector canvas engine & exportToBlob PNG export
Tailwind CSS v4Design tokens, fluid typography & dark-mode styling

AI & Diagram Engine

Google Gemini 2.5Multi-prompt structured diagram generation (@google/genai)
Zod Schema ValidatorRuntime geometry, bounds & spatial overlap constraint enforcement
RenderAIDiagramCustom canvas ingestion & Excalidraw element constructor

Database & Storage

Neon PostgreSQLServerless PostgreSQL with connection pooling & instant scaling
Drizzle ORMType-safe database schemas, relations & migrations
WebP RasterizerBase64 WebP preview thumbnail generation for board cards

Auth & Workspace Infrastructure

Clerk AuthUser authentication, route middleware & session resolution
Workspace EngineBoard listing, search, sort, soft delete & archive restore
09//RESULTS & OUTCOMES

Implementation Status & Roadmap

Excalidraw Vector Canvas & Custom Tools

Full Excalidraw integration with custom floating toolbars (shapes, text, arrows, pencil, eraser, pan).

Context-Aware Floating Element Inspector

Context-aware inspector positioned over selected elements (stroke color, background, stroke width, roughness, opacity, font size, layer ordering, duplication, deletion).

Gemini 2.5 AI Diagram Generator

Multi-prompt generation (System Architecture, Flowcharts, Mindmaps) via Google Gemini API with Zod overlap/bounds validation.

Debounced 10s Autosave & State Normalization

Autosave saving elements, files, normalized appState (preventing viewport distortion and runtime Map crashes), and Base64 WebP preview thumbnails to Neon DB.

Board Management & Soft Delete / Archive

Dashboard with Grid/List views, live search, sorting, soft-delete via isArchived, and dedicated /archived restore and purge pages.

PNG Canvas Export & Tab Switching

Export canvas to PNG via exportToBlob, board title display, and tab switching between Board and Document views.

Roadmap: SmartDoc Markdown & Rich-Text Editor

Rich text / Markdown document editor (TipTap / Lexical) with autosave and database persistence linked to the active projectId.

Roadmap: SaaS Landing Page with Interactive Showcase

Modern SaaS landing page with hero banner, feature highlights, canvas preview demonstration, and authentication CTAs.

Roadmap: Workspace Header Actions & Collaborative Share

Manual instant save button bypassing debounce, and a Share modal with public/collaborative URL and permissions.

Roadmap: AI Credits Metering & Deduction System

Credit balance verification in /api/ai before generation, balance decrement on success, and out-of-credits upgrade modal.

10//LESSONS LEARNED

Engineering Reflections

1. Geometry validation is essential for AI-generated diagrams: Language models cannot reliably estimate spatial collision without guardrails. Enforcing an intermediate Zod schema with boundary checks turns stochastic LLM outputs into clean, readable diagrams.

2. Never serialize raw third-party canvas appState: Excalidraw maintains ephemeral runtime state and JavaScript Map objects. Normalizing appState prior to database storage prevents viewport distortion and JSON serialization crashes.

3. Debouncing paired with client-side WebP thumbnails saves database load: Batching canvas saves into 10-second debounce windows while generating WebP preview thumbnails directly in the browser delivers sub-second dashboard rendering with zero database connection exhaustion.