Loading…
The repo's CLAUDE.md and .ai-rules-universal.md are the longer canonical references. This
page covers the everyday rules.
any. If you absolutely must, use unknown and narrow, or document why with a
comment. Pre-existing any is gradually being replaced.// @ts-ignore without a comment explaining the underlying type bug + how to remove it.tsconfig.json. Don't loosen it.type for unions/intersections, interface for object shapes.import type + export type so they tree-shake cleanly.Sorted into groups by eslint-plugin-import rules:
node: builtins (e.g. node:crypto).@scope/pkg packages.@matters/lib, @matters/auth, …).@/lib/..., @/components/...)../foo, ../bar).Blank line between groups. The autofix handles it: pnpm run lint:fix.
kebab-case for files: lib/auth/email-allowlist.ts, apps/workspace/app/(workspace)/sales/page.tsx.PascalCase.tsx: components/ui/Button.tsx.<file>.test.ts next to source, or __tests__/ for shared fixtures.createApiHandler (lib/api/wrapper.ts) for new
and refactored API routes. It wraps the handler with uniform exception→status mapping,
request logging, rate-limiting and CORS, so the handler just gates with checkRole,
validates with validateInput, throws typed exceptions, and returns the happy path. The
CPT routes (industries / use-cases / videos) are the reference shape. Many older routes
still hand-roll try/catch — migrate them to the wrapper when you touch them.@matters/lib/errors/exceptions (NotFoundError,
ForbiddenError, ValidationError, ExternalServiceError, …), not raw Error. The
wrapper / handleApiError maps each to the correct status code.logger.error(...) first, then return a typed response — don't let
exceptions bubble to Next.js's default handler.error.message in 4xx/5xx responses unless you're 100% sure it's safe to
expose. Most aren't.logger from @/lib/errors/logger. Not console.log.debug. Use info for "expected milestone happened", warn for "unexpected
but recoverable", error for "actual failure that needs Sentry attention".logger.info("Webinar published", { webinarId, slug }), not
logger.info("Webinar published: " + webinarId).const { userId } = await auth(); for the userId, await checkRole(...)
for permission gates.await currentUser() and await requirePermission(...) from
lib/auth/guards.ts.{ error, issues: parsed.error.issues }.lib/utils/validation.ts (emailSchema, urlSchema, etc.)."use client" only when you need state, effects, or
browser APIs.@/lib/db/mongodb,
@matters/lib/config/env's process.env.* access).<button> must set type="button" unless it genuinely submits a form — the
HTML default is type="submit", which silently submits the nearest form. The shared
Button components (components/ui/button.tsx, components/Button.tsx) now default to
type="button"; raw <button> elements set it explicitly.role="dialog" + aria-modal="true" + a label + an
Escape-to-close handler. Prefer the shared Dialog primitive; if you hand-roll (to keep
the ws-* theme), mirror the snippet-generator import modal.aria-label.@matters/theme — ws-* and shadcn semantic
tokens, never hardcoded hex / zinc-* / rounded-* / shadow-* / backdrop-blur. Shared
primitives (DarkInput/DarkTextArea/DarkSelect, GlassCard, SmartInput) are
tokenized — keep them that way so all 12 theme variants + light/dark re-skin for free. Full
rules: apps/workspace/CLAUDE.md and packages/theme/README.md.styled-components.cn(...) from @matters/lib/utils/cn (re-exported as @/lib/utils) to compose
conditional classes.tailwind.config.ts theme.extend.colors.brand. Use the tokens, not
raw hex.// TODO(eshank, 2026-05-30): wire to feature flag service.*.test.ts). Integration tests in tests/integration/.await in tight loops over arrays — batch with Promise.all or runWithConcurrency
(see sync-salesforce/route.ts for the pattern).next/dynamic
(ssr:false) so they only ship to the routes/interactions that use them — e.g. the
observability panels and the snippet generator's WYSIWYG. Register big barrel packages in
next.config.ts → experimental.optimizePackageImports (today: ["sharp", "framer-motion"]).