Skip to content

Instantly share code, notes, and snippets.

@hbinduni
Created June 22, 2026 06:06
Show Gist options
  • Select an option

  • Save hbinduni/62c26e8705ceeee9f3a36b9eadb47ec6 to your computer and use it in GitHub Desktop.

Select an option

Save hbinduni/62c26e8705ceeee9f3a36b9eadb47ec6 to your computer and use it in GitHub Desktop.
a CLAUDE.md by heriyanto binduni for working on lara app

CLAUDE.md

Working context for AI agents on lara-app — a multi-tenant WhatsApp automation platform. Read this before writing code. Non-negotiables: run tasks via the Makefile targets (they wrap Bun + Turbo) · use Bun for package installs (never npm/yarn) · build shared first · run make type-check && make lint before claiming a task done · prefer existing patterns/utilities over inventing new ones. The Critical Gotchas section is hard-won from production — every item is a bug that already bit us once. Treat it as load-bearing.

Project Overview

TypeScript monorepo: Bun runtime + Turbo build + Hono backend + React/Vite/TanStack Router frontend. Multi-tenant WhatsApp automation platform with JWT auth, subscription billing (Midtrans), and tier-based quotas.

Key Commands

# Development
make dev                 # Start all (client:5173, server:3000)
make dev-client          # Client only
make dev-server          # Server only

# Build
make build               # Build all with Turbo caching
make build-shared        # Build shared first (required)

# Quality
make lint                # Lint, format, and autofix
make type-check          # Type-check all packages

# Tests
make test                # All tests
make test-auth           # Auth tests

# Database (wraps scripts/db-manager.ts, reads DATABASE_URL from server/.env)
make db-fresh            # Drop + create + seed (DEV ONLY)
make db-migrate          # Run migrations from db/migrations/
make db-status           # Show tables with row counts
make db-add-user EMAIL=<email> USERNAME=<username> PHONE=<phone> [PASSWORD=<pw>] [TENANT_ID=<id>]

Architecture

lara-app/
├── shared/     # Types (build first!) - import as "shared" (client) or "shared/dist" (server)
├── server/     # Hono API - entry: src/index.ts
├── client/     # React + Vite - entry: src/main.tsx, routes: src/routes/
├── db/         # schema.sql, seed.sql, migrations/
├── k8s/        # Kubernetes manifests (configmap, secret, deployments)
└── scripts/    # db-manager.ts, etc.

Key Patterns

  • Shared Types: Edit shared/src/types/, rebuild, then import
  • Server Routes: Hono method chaining in server/src/routes/
  • Client Routes: File-based in client/src/routes/ (TanStack Router)
  • Auth: JWT (15min access, 7d refresh), authMiddleware, requireSystemRole
  • Multi-Tenant: V3 architecture — each user has waServiceId + waPhone, resolved via wa_services table
  • Roles: System (user, admin, root) + Tenant (owner, admin, manager, user, viewer)
  • Tiers: free | pro | enterprise with quota enforcement via @middleware/quota
  • Encryption: API keys encrypted at rest (AES-256-GCM) via @lib/encryption. Always use decryptSecret() when reading ws.api_key or users.wa_api_key from DB
  • Audit Logging: Fire-and-forget via auditLog() from @lib/audit

Import Aliases

// Client: @/* → src/*, @components/*, @lib/*, @hooks/*, etc.
// Server: @/* → src/*, @lib/*, @routes/*, @middleware/*
// Use @/types/* NOT @types/* (conflicts with TS declarations)
import type { AuthUser } from '@shared'

Code Style (Biome)

  • 2 spaces, 120 chars, single quotes, no semicolons
  • Arrow parens: always, trailing comma: ES5, JSX: double quotes

Environment Variables

Client: VITE_API_BASE_URL, VITE_MIDTRANS_CLIENT_KEY, VITE_MIDTRANS_IS_PRODUCTION

Server: DATABASE_URL, JWT_SECRET, REDIS_URL, ENCRYPTION_KEY, MIDTRANS_SERVER_KEY, MIDTRANS_CLIENT_KEY, GROQ_API_KEY, META_WHATSAPP_*, NATS_URL, S3_*, SENTRY_*

Critical Gotchas

API Key Encryption

// WRONG - passes encrypted "enc:..." string to external API
headers: {'X-API-Key': row.api_key}

// CORRECT - decrypt before use
import { decryptSecret } from '@lib/encryption'
headers: {'X-API-Key': decryptSecret(row.api_key)}

// Note: decryptSecret() is migration-friendly — plaintext values pass through unchanged

JSONB Serialization

// WRONG - breaks jsonb_array_length()
${JSON.stringify(data)}

// CORRECT
${sql.json(data)}

// For complex types (e.g. SubscriptionPlan[]):
${sql.json(value as unknown as Parameters<typeof sql.json>[0])}

PgBouncer Array/JSONB Parsing

With PgBouncer (prepare: false), PostgreSQL returns TEXT[] and JSONB as strings instead of parsed objects.

// WRONG - will fail or corrupt data when PgBouncer returns strings
tags: row.tags || []
recipients: row.recipients

// CORRECT - use shared helpers from @lib/db
import { parseTextArrayField, parseJsonbField } from '@lib/db'

// For TEXT[] columns (tags, scope_jids, custom_columns, etc.)
tags: parseTextArrayField(row.tags as unknown as string[] | string)

// For JSONB columns (recipients, webhook_headers, etc.)
recipients: parseJsonbField(row.recipients, [])
contentApiHeaders: parseJsonbField(row.contentApiHeaders, {})

Affected column types:

  • TEXT[]: Returns as {tag1,tag2} string literal
  • JSONB: Returns as '{"key":"value"}' JSON string

Files with TEXT[] columns: users.tags, scheduled_campaigns.tags, broadcast_*_templates.*, auto_response_rules.scope_jids/exclude_jids

Local Development Database Access

For local development, use direct PostgreSQL (port 5432) instead of PgBouncer (port 5433) to avoid:

  • PgBouncer authentication complexity (userlist.txt, auth_query)
  • TEXT[]/JSONB string parsing issues
  • Prepared statement restrictions
# SSH tunnel configuration (~/.ssh/config)
Host k3s-tunnel
  LocalForward 5432 10.x.x.x:5432   # Direct PostgreSQL (db-rw)
  LocalForward 5433 10.x.x.y:5432   # PgBouncer (db-pgbouncer-rw)

# Use port 5432 in server/.env for local dev
DATABASE_URL=postgresql://app:password@localhost:5432/<db_name>

Note: PgBouncer is still valuable for production (connection pooling, HA), but direct PostgreSQL is simpler for local development.

Tailwind Dynamic Classes

// WRONG - Tailwind uses static analysis
hover:${variable}

// CORRECT
hover:brightness-110

WhatsApp Media

  • Proxy URLs need JWT tokens (auto-generated via /download endpoint)
  • Falls back to WhatsApp CDN on proxy failure

Test Users (from seed.sql)

Auth uses passwordless OTP (WhatsApp or email). In dev mode with OTP_WHATSAPP_PROVIDER=mock, the OTP code is logged to console.

  • app@example.com — Root user (system admin, wa_phone: 62XXXXXXXXXXX)

Important

  • Always use bun, not npm/yarn
  • Build shared before other packages
  • Use Layout + ProtectedRoute for new pages
  • Glassmorphism: bg-white/70 backdrop-blur-xl ring-1 ring-white/50
  • Always decryptSecret() when reading API keys from DB
  • Use auditLog() for security-sensitive operations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment