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>
15 KiB
Executable File
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.DBfor 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 tablessrc/db/schema-plugins.ts- plugins, plugin_config, plugin_eventssrc/db/schema-theme.ts- custom_themes, user_theme_preference
authentication & middleware
- WorkOS handles SSO, email/password, directory sync
- middleware in
src/middleware.tschecks session and redirects unauthenticated users to/login - public paths:
/,/login,/signup,/reset-password,/verify-email,/invite,/callback,/api/auth/*,/api/netsuite/* getCurrentUser()fromlib/auth.tsreturns user info with database lookup fallback
ai agent harness
- located in
src/lib/agent/- a complete AI-assisted systemprovider.ts: OpenRouter setup for kimi-k2.5 modeltools.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 contextcatalog.ts: component specs for DynamicUI renderingchat-adapter.ts: getTextFromParts, action registry, tool dispatch
src/app/api/agent/route.ts: streamText endpoint with 10-step multi-tool loopsrc/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 + persistenceChatView variant="page"on /dashboard (hero idle, typewriter, stats)ChatView variant="panel"inChatPanelShellon all other pagessrc/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_preferencetables track tokens/costs per conversation, per-user model override if admin allows - ai sdk v6 gotchas:
inputSchemanotparametersfor tool() definitions- UIMessage uses
partsarray, no.contentfield - useChat:
sendMessage({ text })notappend({ role, content }) - useChat:
statusis "streaming"|"submitted"|"ready"|"error", notisGenerating - useChat: needs
transport: new DefaultChatTransport({ api })notapiprop - 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 buildersauth/: oauth 2.0 flow, token manager, AES-GCM encryptionclient/: base HTTP client (retry, circuit breaker), record client, suiteql clientrate-limiter/: semaphore-based concurrency limiter (15 concurrent default)sync/: sync engine, delta sync, conflict resolver, push logic, idempotencymappers/: 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 behindisNative()checks, components returnnullon web - platform detection:
src/lib/native/platform.tsexportsisNative(),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.mdfor 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, SkillFrontmatterskills-client.ts: fetchSkillFromGitHub, parseSkillMdloader.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_eventstables insrc/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, ThemeTokenspresets.ts: all 10 preset definitionsapply.ts:applyTheme()injects css vars into<style id="compass-theme-vars">, instant without page reloadfonts.ts:loadGoogleFonts()dynamic<link>injection
src/app/actions/themes.ts: getUserThemePreference, setUserThemePreference, saveCustomTheme, deleteCustomTheme- database:
custom_themes,user_theme_preferencetables insrc/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, impersonationmapper.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_filestables,users.google_emailcolumn - env: GOOGLE_SERVICE_ACCOUNT_ENCRYPTION_KEY
- see
docs/google-drive/GOOGLE-DRIVE-INTEGRATION.mdfor full setup guide
typescript discipline
- strict types: no
any, noas, no!- useunknownwith proper narrowing - discriminated unions over optional properties:
{ status: 'ok'; data: T } | { status: 'error'; error: E } readonlyeverywhere mutation isn't intended:ReadonlyArray<T>,Readonly<Record<K, V>>- no
enum- useas constobjects 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.tsfor 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