compassmock/CLAUDE.md
Nicholai 404a881758
docs(claude): add mobile, themes, plugins, drive (#54)
Document recent features added since initial CLAUDE.md:
- Capacitor mobile app architecture and native hooks
- Plugin/skills system with registry pattern
- Visual theme system with 10 presets + AI generation
- Google Drive integration via service account
- Updated agent harness to reflect unified chat arch
- Expanded project structure and schema file listing

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-07 02:27:53 -07:00

15 KiB
Executable File

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

compass

a construction project management system built with Next.js 15 + React 19, designed to replace BuilderTrend with a lean, self-hosted, open-source alternative. deployed to Cloudflare Workers via OpenNext.

quick start

bun dev          # turbopack dev server on :3000
bun run build        # production build
bun preview      # test build on cloudflare runtime
bun deploy       # build and deploy to cloudflare workers
bun lint         # run eslint

database commands:

bun run db:generate      # generate drizzle migrations from schema
bun run db:migrate:local # apply migrations locally
bun run db:migrate:prod  # apply migrations to production D1

mobile (capacitor):

bun cap:sync             # sync web assets + plugins to native projects
bun cap:ios              # open xcode project
bun cap:android          # open android studio project

tech stack

layer tech
framework Next.js 15 App Router, React 19
language TypeScript 5.x
ui shadcn/ui (new-york style), Tailwind CSS v4
charts Recharts
database Cloudflare D1 (SQLite) via Drizzle ORM
auth WorkOS (SSO, directory sync)
ai agent AI SDK v6 + OpenRouter (kimi-k2.5 model)
integrations NetSuite REST API, Google Drive API
mobile Capacitor (iOS + Android webview)
themes 10 presets + AI-generated custom themes (oklch)
state React Context, server actions

critical architecture patterns

server actions & data flow

  • all data mutations go through server actions in src/app/actions/
  • pattern: return { success: true } | { success: false; error: string }
  • server actions revalidate paths with revalidatePath() to update client
  • no fetch() in components - use actions instead
  • environment variables accessed via getCloudflareContext()env.DB for D1

database

  • drizzle ORM with D1 (SQLite dialect)
  • text IDs (UUIDs), text dates (ISO 8601 format)
  • migrations in drizzle/ directory - add new migrations, never modify old ones
  • schema files:
    • src/db/schema.ts - core tables (users, projects, customers, vendors, etc.)
    • src/db/schema-netsuite.ts - netsuite sync tables
    • src/db/schema-plugins.ts - plugins, plugin_config, plugin_events
    • src/db/schema-theme.ts - custom_themes, user_theme_preference

authentication & middleware

  • WorkOS handles SSO, email/password, directory sync
  • middleware in src/middleware.ts checks session and redirects unauthenticated users to /login
  • public paths: /, /login, /signup, /reset-password, /verify-email, /invite, /callback, /api/auth/*, /api/netsuite/*
  • getCurrentUser() from lib/auth.ts returns user info with database lookup fallback

ai agent harness

  • located in src/lib/agent/ - a complete AI-assisted system
    • provider.ts: OpenRouter setup for kimi-k2.5 model
    • tools.ts: queryData, navigateTo, showNotification, renderComponent, plus theme tools (listThemes, setTheme, generateTheme, editTheme) and plugin tools (installSkill, uninstallSkill, toggleInstalledSkill, listInstalledSkills)
    • system-prompt.ts: dynamic prompt builder with page/user context
    • catalog.ts: component specs for DynamicUI rendering
    • chat-adapter.ts: getTextFromParts, action registry, tool dispatch
  • src/app/api/agent/route.ts: streamText endpoint with 10-step multi-tool loop
  • src/app/actions/agent.ts: D1 persistence (save/load/delete conversations)
  • unified chat architecture: one component, two presentations
    • ChatProvider (layout level) owns chat state + panel open/close + persistence
    • ChatView variant="page" on /dashboard (hero idle, typewriter, stats)
    • ChatView variant="panel" in ChatPanelShell on all other pages
    • src/hooks/use-compass-chat.ts: shared hook (useChat + action handlers + tool dispatch)
    • chat state persists across navigation
  • usage tracking: agent_config, agent_usage, user_model_preference tables track tokens/costs per conversation, per-user model override if admin allows
  • ai sdk v6 gotchas:
    • inputSchema not parameters for tool() definitions
    • UIMessage uses parts array, no .content field
    • useChat: sendMessage({ text }) not append({ role, content })
    • useChat: status is "streaming"|"submitted"|"ready"|"error", not isGenerating
    • useChat: needs transport: new DefaultChatTransport({ api }) not api prop
    • zod schemas must use import { z } from "zod/v4" to match AI SDK internals
    • ToolUIPart: properties may be flat or nested under toolInvocation

netsuite integration

  • full bidirectional sync via REST API
  • key files in src/lib/netsuite/:
    • config.ts: account setup, URL builders
    • auth/: oauth 2.0 flow, token manager, AES-GCM encryption
    • client/: base HTTP client (retry, circuit breaker), record client, suiteql client
    • rate-limiter/: semaphore-based concurrency limiter (15 concurrent default)
    • sync/: sync engine, delta sync, conflict resolver, push logic, idempotency
    • mappers/: customer, vendor, project, invoice, vendor-bill mappers
  • env vars: NETSUITE_CLIENT_ID, NETSUITE_CLIENT_SECRET, NETSUITE_ACCOUNT_ID, NETSUITE_REDIRECT_URI, NETSUITE_TOKEN_ENCRYPTION_KEY, NETSUITE_CONCURRENCY_LIMIT
  • gotchas:
    • 401 errors can mean timeout, not auth failure
    • "field doesn't exist" often means permission denied
    • 15 concurrent request limit shared across ALL integrations
    • no batch create/update via REST (single record per request)
    • sandbox URLs use different separators (123456-sb1 vs 123456_SB1)
    • omitting "line" param on line items adds new line (doesn't update)

capacitor mobile app

  • webview-based native wrapper loading the live cloudflare deployment (not a static export)
  • the web app must never break because of native code: all capacitor imports are dynamic (await import() inside effects), gated behind isNative() checks, components return null on web
  • platform detection: src/lib/native/platform.ts exports isNative(), isIOS(), isAndroid()
  • native hooks in src/hooks/: use-native.ts, use-native-push.ts, use-native-camera.ts, use-biometric-auth.ts, use-photo-queue.ts
  • native components in src/components/native/: biometric lock screen, offline banner, status bar sync, upload queue indicator, push registration
  • offline photo queue (src/lib/native/photo-queue.ts): survives app kill, uses filesystem + preferences + background uploader
  • push notifications via FCM HTTP v1 (src/lib/push/send.ts), routes to both iOS APNS and Android FCM
  • src/app/api/push/register/route.ts: POST/DELETE for device token management
  • env: FCM_SERVER_KEY
  • see docs/native-mobile.md for full iOS/Android setup and app store submission guide

plugin/skills system

  • agent can install external "skills" (github-hosted SKILL.md files in skills.sh format) or full plugin modules
  • skills inject system prompt sections at priority 80, full plugins can also provide tools, components, query types, and action handlers
  • source types: builtin, local, npm, skills
  • key files in src/lib/agent/plugins/:
    • types.ts: PluginManifest, PluginModule, SkillFrontmatter
    • skills-client.ts: fetchSkillFromGitHub, parseSkillMd
    • loader.ts: loadPluginModule (local/npm/builtin)
    • registry.ts: buildRegistry, getRegistry (30s TTL cache per worker isolate)
  • src/app/actions/plugins.ts: installSkill, uninstallSkill, toggleSkill, getInstalledSkills
  • database: plugins, plugin_config, plugin_events tables in src/db/schema-plugins.ts

visual theme system

  • per-user themeable UI with 10 built-in presets + AI-generated custom themes
  • themes are full oklch color maps (32 keys for light + dark), fonts (sans/serif/mono with google fonts), design tokens (radius, spacing, shadows)
  • presets: native-compass (default), corpo, notebook, doom-64, bubblegum, developers-choice, anslopics-clood, violet-bloom, soy, mocha
  • key files in src/lib/theme/:
    • types.ts: ThemeDefinition, ColorMap, ThemeFonts, ThemeTokens
    • presets.ts: all 10 preset definitions
    • apply.ts: applyTheme() injects css vars into <style id="compass-theme-vars">, instant without page reload
    • fonts.ts: loadGoogleFonts() dynamic <link> injection
  • src/app/actions/themes.ts: getUserThemePreference, setUserThemePreference, saveCustomTheme, deleteCustomTheme
  • database: custom_themes, user_theme_preference tables in src/db/schema-theme.ts

google drive integration

  • domain-wide delegation via service account (impersonates users by email)
  • two-layer permissions: compass RBAC first, then google workspace permissions
  • bidirectional: browse drive files in compass, upload from compass to drive, supports shared drives
  • key files in src/lib/google/:
    • auth/service-account.ts: JWT creation, token exchange (web crypto API, RS256)
    • client/drive-client.ts: REST client with retry, rate limiting, impersonation
    • mapper.ts: DriveFile -> FileItem
  • src/app/actions/google-drive.ts: 17 server actions (connect, disconnect, list, search, create, rename, move, trash, restore, upload, etc.)
  • file browser UI in src/components/files/
  • database: google_auth, google_starred_files tables, users.google_email column
  • env: GOOGLE_SERVICE_ACCOUNT_ENCRYPTION_KEY
  • see docs/google-drive/GOOGLE-DRIVE-INTEGRATION.md for full setup guide

typescript discipline

  • strict types: no any, no as, no ! - use unknown with proper narrowing
  • discriminated unions over optional properties: { status: 'ok'; data: T } | { status: 'error'; error: E }
  • readonly everywhere mutation isn't intended: ReadonlyArray<T>, Readonly<Record<K, V>>
  • no enum - use as const objects or union types instead
  • branded types for primitive identifiers to prevent mixing up IDs of different types
  • explicit return types on all exported functions
  • result types over exceptions: return { success: true } | { success: false; error } from actions
  • effect-free module scope: no console.log, fetch, or mutations during import

project structure

src/
├── app/                    # next.js app router
│   ├── (auth)/            # auth pages (login, signup, etc)
│   ├── api/               # api routes (agent, push, netsuite, auth)
│   ├── dashboard/         # protected dashboard routes
│   ├── actions/           # server actions (data mutations)
│   ├── globals.css        # tailwind + theme variables
│   ├── layout.tsx         # root layout
│   └── page.tsx           # home page
├── components/            # react components
│   ├── ui/               # shadcn/ui primitives (auto-generated)
│   ├── agent/            # ai chat (ChatView, ChatProvider, ChatPanelShell)
│   ├── native/           # capacitor mobile components
│   ├── netsuite/         # netsuite connection ui
│   ├── files/            # google drive file browser
│   └── *.tsx             # app-specific components
├── db/
│   ├── index.ts          # getDb() function
│   ├── schema.ts         # core drizzle schema
│   ├── schema-netsuite.ts # netsuite sync tables
│   ├── schema-plugins.ts  # plugin/skills tables
│   └── schema-theme.ts    # theme tables
├── hooks/                 # custom react hooks (incl. native hooks)
├── lib/
│   ├── agent/            # ai agent harness + plugins/
│   ├── google/           # google drive integration
│   ├── native/           # capacitor platform detection + photo queue
│   ├── netsuite/         # netsuite integration
│   ├── push/             # push notification sender
│   ├── theme/            # theme presets, apply, fonts
│   ├── auth.ts           # workos integration
│   ├── permissions.ts    # rbac checks
│   ├── utils.ts          # cn() for class merging
│   └── validations/      # zod schemas
└── types/                # global typescript types

ios/                       # xcode project (capacitor)
android/                   # android studio project (capacitor)
drizzle/                   # database migrations (auto-generated)
docs/                      # user documentation
public/                    # static assets
capacitor.config.ts        # capacitor native config
wrangler.jsonc             # cloudflare workers config
drizzle.config.ts          # drizzle orm config
next.config.ts             # next.js config
tsconfig.json              # typescript config

component conventions

shadcn/ui setup:

  • new-york style
  • add components: bunx shadcn@latest add <component-name>
  • aliases: @/components, @/components/ui, @/lib, @/hooks

ui patterns:

  • use cn() from @/lib/utils.ts for conditional classes
  • form validation via react-hook-form + zod
  • animations via framer-motion or tailwind css
  • icons from lucide-react or @tabler/icons-react (both configured with package import optimization)
  • data tables via tanstack/react-table

environment variables

dev: .dev.vars (gitignored) prod: cloudflare dashboard secrets

required:

  • WORKOS_API_KEY, WORKOS_CLIENT_ID
  • OPENROUTER_API_KEY (for ai agent)
  • NETSUITE_* (if using netsuite sync)
  • GOOGLE_SERVICE_ACCOUNT_ENCRYPTION_KEY (if using google drive)
  • FCM_SERVER_KEY (if using push notifications)

development tips

accessing database in actions

import { getCloudflareContext } from "@opennextjs/cloudflare"
const { env } = await getCloudflareContext()
const db = env.DB // D1 binding

using getCurrentUser() in actions

import { getCurrentUser } from "@/lib/auth"
const user = await getCurrentUser()
if (!user) throw new Error("Not authenticated")

revalidating paths after mutations

import { revalidatePath } from "next/cache"
// after changing data
revalidatePath("/dashboard/projects") // specific path
revalidatePath("/", "layout") // entire layout

querying data in components

  • server components can use getDb() directly
  • client components must call server actions
  • never fetch() from client components - use actions

type guards for discriminated unions

don't use as - write proper type guards:

function isError<E>(result: { success: boolean; error?: E }): result is { success: false; error: E } {
  return !result.success && result.error !== undefined
}

known issues & WIP

  • gantt chart vertical panning: zoom/horizontal pan work, but vertical pan conflicts with frappe-gantt container sizing. needs transform-based rendering approach with fixed header.

open source contribution notes

  • repo at github.com/High-Performance-Structures/compass (private, invite-only)
  • branching: <username>/<feature> off main
  • conventional commits: type(scope): subject
  • PRs are squash-merged to main
  • deployment to cloudflare is manual via bun deploy