Key architectural decisions and patterns for teams working with this codebase.
React Compiler is enabled. Do not use useMemo, useCallback, or React.memo:
// ❌ Don't
const memoizedValue = useMemo(() => compute(a, b), [a, b])
// ✅ Do
const value = compute(a, b)Exception: Use useRef for object instantiation to prevent infinite loops.
Both are used intentionally:
- Tailwind (80%) — spacing, colors, typography
- CSS Modules — complex animations, custom layouts, CSS specificity
Import CSS modules as s:
import s from './component.module.css'Always use these, never native HTML:
import { Image } from '@/components/ui/image'
import { Link } from '@/components/ui/link'Why?
- Image: Optimization, aspect ratios, WebGL integration
- Link: External detection, prefetching, consistent behavior
Configured in app/layout.tsx. ScrollTrigger uses Lenis automatically.
Root layout conditionally loads features:
import { OptionalFeatures } from '@/lib/features'
<OptionalFeatures /> // Only loads WebGL, dev tools when neededServer Components use advanced caching. Key rules:
| Data Type | Cache Strategy |
|---|---|
| Public content | ISR with revalidate |
| User-specific | cache: 'no-store' |
| Real-time | cache: 'no-store' |
Gotchas:
- Never cache user data (carts, accounts, private content)
- Wrap cached components in Suspense boundaries
- Test with hard refresh AND navigation (different cache layers)
- Use
revalidateTag()orrevalidatePath()for invalidation
Uses lazy GlobalCanvas with visibility-based pausing:
Root Layout → LazyGlobalCanvas (mounts on first WebGL page)
└─ WebGLTunnel (portals 3D content)
└─ Your 3D scene
Context survives navigation. See lib/webgl/README.md.
components/
├── ui/ → Primitives (reusable)
├── layout/ → Site chrome (customize)
└── effects/ → Animations
lib/
├── hooks/ → React hooks + Zustand
├── styles/ → CSS & Tailwind config
├── utils/ → Pure utilities
├── integrations/ → Third-party (optional)
├── webgl/ → 3D graphics (optional)
└── dev/ → Debug tools (optional)
Zod schemas provide type-safe validation at three boundaries:
- Environment variables -- Per-integration schemas validate config at startup via
check-integration.tsanddoctor.ts - Server actions --
parseFormData()validates FormData before processing (HubSpot, Mailchimp, Shopify) - Client forms --
zodToValidator()bridges the same Zod schemas to the form hook's client-side validation
All schemas live in lib/utils/validation.ts. The typed env singleton (lib/env.ts) provides IntelliSense for process.env access.
proxy.ts (project root) handles cross-cutting request concerns for Next.js 16. Currently configured with rate limiting for /api/* routes. Security headers remain in next.config.ts (they're static configuration).
- Environment variables configured
- Build passes (
bun build) - Webhooks configured (Sanity, Shopify)
- Cache invalidation tested
- Performance score > 90
New component: bun run generate or add to components/ui/
New integration: Add Zod env schema in @/utils/validation, add entry in lib/integrations/registry.ts. Everything else (check-integration, doctor) derives automatically from the registry.
Modify styles: Edit config in lib/styles/, run bun setup:styles
Built with Satūs by darkroom.engineering