xCanvas.AI — High-Level Design
AI-powered canvas tool for generating pixel-perfect mobile UI from natural language prompts.
Layer
Technology
Framework
Next.js 15 (App Router)
Language
TypeScript
Auth
Kinde Auth
Database
MongoDB via Prisma ORM
AI
Gemini Flash Lite via OpenRouter AI SDK
Background Jobs
Inngest
Screenshot
Puppeteer + @sparticuz/chromium-min
State Management
TanStack React Query
UI
shadcn/ui + Tailwind CSS
Canvas
react-zoom-pan-pinch
Theming
next-themes
Real-time dynamic canvas with zoom and pan
Multi-frame support — multiple screens side by side
Device frame wrapper for realistic mobile preview
Hand tool and select tool modes
Dot-grid background for professional canvas feel
Generate mobile UI screens from natural language prompts
Powered by Gemini Flash Lite via OpenRouter
Background job processing via Inngest
Loading states with status indicators (fetching, generating, finalizing)
Auto-save canvas thumbnail on generation complete
Manual screenshot download as PNG
Full CSS extraction for accurate capture
Puppeteer powered — chromium-min on Vercel, full puppeteer locally
Chromium path cached to avoid re-downloading on every request
Multiple theme support with CSS variables
Apply and save theme per project
Light and dark mode toggle
Theme persists to database per project
Create and name projects
Project cards with thumbnail preview
Relative timestamps (e.g. "2 hours ago")
Delete project with confirmation dialog
Auto-refresh project list after deletion
Kinde Auth — login, logout, session management
User avatar with dropdown menu
Protected API routes — all endpoints require auth
Per-user project isolation
MongoDB via Prisma ORM
Project model — name, theme, thumbnail, frames
Frame model — title, htmlContent, linked to project
Child frames deleted before project (manual cascade)
shadcn/ui component library
Tailwind CSS styling
Fully responsive
Toast notifications for all actions
Confirm dialogs for destructive actions
Skeleton loaders while content loads
Hydration-safe theme toggle
xCanvas.AI/
├── app/
│ ├── (routes)/
│ │ ├── _common/
│ │ │ └── Header.tsx # Global header — theme toggle + Kinde auth
│ │ └── p/
│ │ └── [id]/
│ │ └── page.tsx # Project canvas page
│ ├── api/
│ │ ├── project/
│ │ │ └── [id]/
│ │ │ └── route.ts # PATCH — update project theme
│ │ ├── screenshot/
│ │ │ └── route.ts # POST — capture canvas HTML as PNG / save thumbnail
│ │ └── inngest/
│ │ └── route.ts # Inngest webhook handler
│ └── layout.tsx # Root layout — <Toaster />, providers
│
├── components/
│ ├── canvas/
│ │ ├── canvas.tsx # Main canvas orchestrator
│ │ ├── canvas-controls.tsx # Zoom in/out, tool switcher
│ │ ├── canvas-floating-toolbar.tsx # Save theme + screenshot buttons
│ │ ├── canvas-loader.tsx # Overlay with loading status
│ │ ├── device-frame.tsx # Phone frame wrapping HTML iframe
│ │ ├── frame-skeleton.tsx # Skeleton while frame loads
│ │ └── html-dialog.tsx # View/edit raw HTML dialog
│ ├── logo.tsx
│ └── ui/ # shadcn/ui components
│
├── context/
│ └── canvas-provider.tsx # Canvas state — frames, theme, loadingStatus
│
├── hooks/
│ └── use-update-project.ts # React Query mutation — update project theme
│
├── inngest/
│ └── functions.ts # Inngest background job definitions
│
├── lib/
│ ├── prisma.ts # Prisma client singleton
│ └── utils.ts # cn() and helpers
│
├── constant/
│ └── canvas.ts # TOOL_MODE_ENUM, type definitions
│
├── types/
│ └── index.ts # Shared TypeScript types
│
└── prisma/
└── schema.prisma # MongoDB schema — Project, User, Frame models
Method
Route
Auth
Description
PATCH
/api/project/[id]
✅ Kinde
Update project theme
POST
/api/screenshot
✅ Kinde
Render HTML → PNG, optionally save as thumbnail
POST
/api/inngest
Inngest
Background job webhook handler
1. User Generates UI (AI Prompt → Canvas Frame)
User types prompt
→ AI SDK calls Gemini Flash Lite via OpenRouter
→ Inngest background job queued
→ HTML generated for each screen
→ canvas-provider updates frames[]
→ DeviceFrame renders HTML in isolated iframe
→ loadingStatus: "generating" → "completed"
loadingStatus === "completed"
→ Canvas.tsx useEffect triggers
→ getCanvasHtmlContent() — extracts outerHTML + all CSS rules
→ POST /api/screenshot { html, width, height, projectId }
→ Puppeteer launches (chromium-min on Vercel / puppeteer locally)
→ page.setContent(html) → page.screenshot() → PNG buffer
→ Prisma: project.update({ thumbnail: base64 })
→ toast.success("Thumbnail Saved!")
3. Manual Screenshot Download
User clicks Screenshot button
→ handleScreenShot()
→ POST /api/screenshot { html, width, height } ← no projectId
→ Returns raw PNG blob
→ Browser download triggered as .png file
→ toast.success("Screenshot downloaded")
User selects theme → clicks Save
→ useUpdateProject.mutate(themeId)
→ PATCH /api/project/[id] { themeId }
→ Prisma: project.update({ theme: themeId })
→ toast.success("Project Updated")
RootLayout
└── ThemeProvider (next-themes)
└── QueryClientProvider (TanStack)
└── Toaster (sonner)
├── Header
│ ├── Logo
│ ├── ThemeToggle (Sun/Moon — mounted guard)
│ └── UserDropdown (Kinde Avatar + Logout)
│
└── ProjectPage (/p/[id])
└── CanvasProvider (context)
└── Canvas
├── CanvasFloatingToolbar
│ ├── SaveThemeButton → useUpdateProject
│ └── ScreenshotButton → handleScreenShot
├── CanvasLoader (overlay)
├── TransformWrapper (zoom/pan)
│ ├── TransformComponent
│ │ └── DeviceFrame[] (per frame)
│ │ └── iframe (isolated HTML + theme vars)
│ └── CanvasControls (zoom %, hand/select tool)
└── HtmlDialog (view raw HTML)
Database Schema (Prisma / MongoDB)
model Project {
id String @id @default (auto () ) @map (" _id " ) @db.ObjectId
userId String
name String
theme String ? // themeId applied to canvas
thumbnail String ? // base64 PNG saved on generation
createdAt DateTime @default (now () )
updatedAt DateTime @updatedAt
frames Frame []
}
model Frame {
id String @id @default (auto () ) @map (" _id " ) @db.ObjectId
projectId String @db.ObjectId
title String
htmlContent String
project Project @relation (fields : [projectId ] , references : [id ] )
}
# Auth
KINDE_CLIENT_ID =
KINDE_CLIENT_SECRET =
KINDE_ISSUER_URL =
KINDE_SITE_URL =
KINDE_POST_LOGOUT_REDIRECT_URL =
KINDE_POST_LOGIN_REDIRECT_URL =
# Database
DATABASE_URL = # MongoDB connection string
# AI
OPENROUTER_API_KEY = # OpenRouter → Gemini Flash Lite
# Background Jobs
INNGEST_EVENT_KEY =
INNGEST_SIGNING_KEY =
Local : Uses full puppeteer (bundled Chromium)
Vercel : Uses puppeteer-core + @sparticuz/chromium-min with remote binary URL
Chromium path is cached across requests to avoid re-downloading on every invocation
Inngest requires npx inngest-cli@latest dev running locally for background jobs