# TOWER — Claude Code Rules ## Project Overview TOWER is a multi-tenant WhatsApp community management platform. It has four portals served by a single Next.js app and a single NestJS API: | Portal | Path prefix | Auth cookie | Guard | |--------|------------|-------------|-------| | Portal selector | `/` | — | public | | Chapter admin | `/`, `/search`, `/groups`, `/messages`, `/drafts`, `/settings`, `/threads`, `/claim-group` | `tower_token` | `JwtAuthGuard` | | Organisation admin | `/org/*` | `tower_org_token` | `OrgAdminGuard` | | Super admin | `/admin/*` | `tower_super_token` | `SuperAdminGuard` | | Member | `/my/*`, `/onboard`, `/member-login` | `tower_member_token` | `MemberAuthGuard` | ## Tech Stack - **Next.js 16** (App Router, TypeScript) — `apps/web` - **NestJS** (TypeScript) — `apps/api` - **Prisma** — ORM, PostgreSQL - **BullMQ** + **Redis** — job queues - **Tailwind CSS v4** — utility-first CSS - **shadcn/ui** — component library (new-york style, CSS variables) - **TanStack React Query** — all data fetching and mutations, no raw `fetch` in components - **Zod** — all schema validation (forms, API response shapes) - **react-hook-form** + **@hookform/resolvers/zod** — all forms - **lucide-react** — icons ## File & Folder Conventions ``` apps/web/app/ _lib/ # Shared utilities and contexts api.ts # getApiBaseUrl, jsonResponse, cookie helpers query-client.tsx # QueryClientProvider wrapper auth-context.tsx # Chapter admin auth (tower_token) super-admin-context.tsx # Super admin auth (tower_super_token) org-admin-context.tsx # Org admin auth (tower_org_token) utils.ts # cn() helper _components/ # Shared UI components (shadcn + custom) ui/ # Raw shadcn components (never edit directly) portal-shell.tsx # Shared portal layout wrapper nav-user.tsx # User dropdown in nav data-table.tsx # Shared table component api/ # Next.js BFF route handlers only auth/… admin/… org/… my/… (public)/ # Portal selector + all login pages (no auth) admin/ # Super admin portal org/ # Org admin portal my/ # Member portal [chapter pages]/ # Chapter admin pages at root level ``` ## Coding Rules ### General - **No raw `fetch` in components.** All API calls go through TanStack Query hooks. Define queries in a co-located `_queries.ts` or inline `useQuery`/`useMutation`. - **No `useState` for server data.** Use React Query. `useState` is for UI-only state (modal open, tab selection, etc.). - **All forms use react-hook-form + Zod.** Schema first: define a `z.object({...})` schema, derive the type with `z.infer`, pass resolver to `useForm`. - **Shared components live in `app/_components/`.** Never duplicate a button, input, or card across portals. - **Each portal has its own layout** with nav/sidebar. Do not put portal UI logic in the root layout. - **No `any` types.** Use proper types or `unknown` + type narrowing. ### TanStack React Query ```tsx // Define query key factories export const tenantKeys = { all: ['tenants'] as const, list: () => [...tenantKeys.all, 'list'] as const, detail: (id: string) => [...tenantKeys.all, 'detail', id] as const, }; // In component const { data, isPending } = useQuery({ queryKey: tenantKeys.list(), queryFn: () => api.get('/api/admin/tenants'), }); // Mutations always invalidate related queries const mutation = useMutation({ mutationFn: (body: CreateTenantInput) => api.post('/api/admin/tenants', body), onSuccess: () => queryClient.invalidateQueries({ queryKey: tenantKeys.all }), }); ``` ### Forms ```tsx const schema = z.object({ email: z.string().email(), password: z.string().min(8), }); type FormValues = z.infer; function MyForm() { const form = useForm({ resolver: zodResolver(schema) }); return (
( Email )} /> ); } ``` ### API Helper Use the typed `api` helper from `_lib/api-client.ts` — never call `fetch` directly in components or hooks: ```ts // app/_lib/api-client.ts export const api = { get: (url: string) => fetch(url, { cache: 'no-store' }).then(r => r.ok ? r.json() as T : Promise.reject(r)), post: (url: string, body: unknown) => fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }).then(r => r.ok ? r.json() as T : Promise.reject(r)), patch: (url: string, body: unknown) => fetch(url, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }).then(r => r.ok ? r.json() as T : Promise.reject(r)), del: (url: string) => fetch(url, { method: 'DELETE' }).then(r => { if (!r.ok) return Promise.reject(r); }), }; ``` ### Auth Contexts Each portal's auth context provides: - `user` / `admin` — the authenticated entity (undefined when loading) - `loading` — boolean - `login(...)` — sets cookie via BFF, updates context - `logout()` — clears cookie, redirects to portal login Auth guard pattern (in portal layout, server component): ```tsx // In a portal layout — always server component import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; export default async function PortalLayout({ children }) { const token = (await cookies()).get('tower_token')?.value; if (!token) redirect('/login'); return {children}; } ``` ### Styling - Use shadcn/ui components as the base. Extend with Tailwind utilities. - **No inline styles.** All styles via Tailwind classes. - **No magic numbers in class names** — use design tokens (`text-sm`, `p-4`, `gap-2` — not arbitrary `p-[13px]`). - Colors: use semantic tokens (`text-foreground`, `bg-muted`, `border`) not raw colors. - Dark mode: set up via `class` strategy but only if explicitly requested. ### NestJS API Rules - All protected endpoints use a Guard decorator — never trust the request without it. - DTOs use `class-validator` decorators. `ValidationPipe` with `whitelist: true, forbidNonWhitelisted: true` is global. - Always use `select` in Prisma queries to avoid leaking `passwordHash` or other sensitive fields. - Org-scoped queries always include `organizationId` in the `where` clause. - Tenant-scoped queries always include `tenantId` in the `where` clause. ### Redirect Rules (IMPORTANT) After successful login, each portal redirects to its own home: - Chapter admin: `/search` (or `next` param if set) - Org admin: `/org` - Super admin: `/admin` - Member: `/my` Never redirect to `/` after login — that is the public portal selector. ### Commit Style Conventional commits: `feat:`, `fix:`, `refactor:`, `chore:` Co-author every commit with Claude.