This file defines how the product is built — technology, routes, structure, and code rules. For visuals see design.md.
| Item | Rule |
|---|---|
| Package manager | PNPM only — pnpm for all package commands. Do not use npm or yarn. |
| Node | 24 |
| Deployment | Ready for Vercel Serverless |
Common commands: pnpm add, pnpm add -D, pnpm remove, pnpm install, pnpm run <script>.
Application: Next.js (App Router) · Tailwind CSS (theme and styling) · Shadcn UI (includes dark mode) · Radix UI (primitives when there is no shadcn/ui equivalent) · React · TanStack Query · React Hook Form · Framer Motion (scroll, animations, gestures)
Data & backend: TanStack Query (@tanstack/react-query) — async state for browser Supabase calls; hooks and helpers in queries/. Supabase — Database, Auth, Storage. Cron is reserved for a post-scheduling stretch feature.
- This is not the Next.js version most training data describes: APIs, conventions, and layout can differ. Before writing Next-specific code, read the relevant guide under
node_modules/next/dist/docs/and heed deprecation notices.
- Explicit types for function parameters and return values.
- Prefer
typefor object shapes (matches@tkodev/config-eslint-next/@typescript-eslint/consistent-type-definitions). - Avoid
any— useunknownwhen the type is truly unknown. - Shared types: app-wide in
types/<name>.tsor next to what they describe (e.g. component props in the same file), per usual TS practice. - Component props: Prefer one
ComponentNamePropstype per file for the primary export. If you would splitBasePropsandComponentProps, merge them into a singleComponentNamePropswhen the base is only used once. Export that type when other modules need the shape (export type ComponentNameProps = …orexport type { ComponentNameProps }). Compound UI that exposes several named subcomponents (e.g.Card+CardHeader) may use one*Propstype per subcomponent; keep those internal unless a consumer needs them. - Cross-cutting mutation/query payloads shared by hooks and callers live in
types/mutations.ts, alongside domain types such astypes/post.ts. - Constants: values under
constants/(and other module-level constants) use camelCase — e.g.maxPostMediaItems,supabaseTablePosts,routeProfiles. Do not useSCREAMING_SNAKE_CASEfor these. Names from the runtime environment (process.env.*) stay as defined by the platform.
- Prefer a single export block, using named exports at the end of the file.
- Except where a tool requires it — e.g.
proxy.tsormiddleware.ts, uses inline export as required by Next.js.
- Except where a tool requires it — e.g.
- Avoid
export default- Except where a tool requires it — e.g.
next.config.ts, uses default export as required by Next.js.
- Except where a tool requires it — e.g.
| Folder | Purpose |
|---|---|
utils/ |
Pure helpers, formatting, small algorithms, integration glue (e.g. Supabase createClient for browser/server, proxy/session helpers, Tailwind cn). |
types/ |
Shared TypeScript shapes used in multiple places (domain models, mutation inputs, etc.). |
queries/ |
TanStack Query only: useMutation / useQuery, mutationFn / queryFn, and keys.ts. No React providers and no generic utilities here. |
constants/ |
App-wide constants in camelCase (Supabase table and bucket names, query defaults, routes, limits such as max post media). |
Providers: Tree wrappers (e.g. TanStack QueryClientProvider) belong in components/providers/, not in queries/. Provider modules are logic/context only: they do not use the cva styling pattern or classNames, and exported components do not need a className prop or other presentational styling API.
- Colocate
useMutation/useQueryand sharedmutationFnhelpers underqueries/. Query keys:queries/keys.ts. - Import hooks and types from the file that defines them (
@/queries/<name>), not from a barrelindex.ts— e.g.@/queries/auth,@/queries/posts. Do not addqueries/index.tsor other re-export aggregators for./queries. - Do not add SWR; it was removed in favour of TanStack Query.
- Avoid
index.ts(or similar) that only re-exports sibling modules. Prefer direct imports from the source file so dependency graphs stay clear and tree-shaking stays predictable.
- Use TanStack Query mutations (and queries if you add client-side reads) under
queries/, withQueryProviderfrom@/components/providers/query-providerin the root layout.
useState— local UI state.- Server Components — initial server data where appropriate.
useOptimistic— optimistic UI where it fits.
- Use React Hook Form (
useForm,register,handleSubmit) for field state and validation on auth pages, settings dialogs, profile edit, and post caption/subtitle/status. - Keep
useStatefor data that is not plain inputs (e.g. post media grid + drag-and-drop, avatar file preview).
- Non–data-fetching reusable UI logic →
hooks/. - Types for that logic follow the TypeScript rules above.
- Pages assemble React components, content, and hooks; they are the main place that defines how a screen is composed.
- All pages share a common header bar. Pages that use a sidebar share the same sidebar component. Visual principles: design.md.
- Support responsive layouts (desktop, tablet, mobile).
- Mobile-first: Treat touch and small viewports as the default. Do not rely on hover to reveal essential controls — patterns such as
opacity-0withgroup-hover:opacity-100, or hiding actions until:hover, fail on touch devices. Prefer always-visible affordances (or touch-friendly alternatives like explicit menus) for anything the user must reach to complete a task. - Keep components presentational: supply content and business logic via props or children, not inside the component.
- Reuse components where it makes sense.
- Add
"use client"only when required. - Dialogs: Always include
<DialogTitle>and<DialogDescription>.
New UI must follow one of the two layouts in components/atoms/example-base.tsx and components/atoms/example-ref.tsx — pick the one that fits.
- Use the file as a starting point: same section order (styles & constants → types → component → exports). Colocate TypeScript props in the same file as a single
MyComponentPropswhen possible. - Compound modules (several related components or
cvahelpers such asbuttonVariants) list every public symbol in the same exports block. - Base (
example-base.tsx): Use when callers do not need a ref to the root DOM node (React.FC<…>). - With ref (
example-ref.tsx): Use when the root must accept a ref — focus,useSortable/measurement, or any parent that passesref(React.forwardRef+displayName).
Do not put class strings (literals, template literals, or cn("a", "b")-style ad hoc lists) in className={} on elements in UI modules. This applies to everything under components/, app/ (pages, layouts, layout.tsx), and other React UI files in the repo.
- Always define styles with
cva()(class-variance-authority), usually on astylesobject whose values arecva(...)builders; usevariants,compoundVariants, anddefaultVariantswhenever classes differ by prop, state, or size. - Every
className={...}should resolve through those builders (e.g.className={cn(styles.root({ className }))}on the root,className={cn(styles.toolbar())}on inner nodes). UseVariantProps<typeof styles.<slot>>where variants apply. - Keep the
stylesobject tidy: everycvaslot must be referenced from the component (or shared intentionally across elements); delete unused slots. - All Tailwind for that file belongs in those
cvadefinitions — do not leave one-off utility strings in JSX. - Narrow exceptions: a third-party API that requires a raw
classNamestring (prefer acvaslot pluscnwhen merging is allowed), or framework glue such asnext/fontvariable classes merged on<html>/<body>in the root layout. - Root
classNameprop: Exported components must acceptclassName?: stringand merge it into the root slot viacva(as in the examples), so parents can extend layout or spacing. - Shared slots: Multiple elements may reuse the same
cvadefinition when classes are identical — e.g. onestyles.iconSmfor several Lucide icons of the same size. You do not need a separatecvaper element if the class string is identical.
The Tailwind subsection below is token/layout guidance; express it via cva in real files, not as raw strings in JSX.
- Follow the Styling (CVA everywhere) rules above: no inline class strings in
className={}— only outputs ofcva()(andcnwhen merging slots orcvaresults). - Prefer semantic tokens (
bg-background,text-foreground, etc.). - Use
gap-*for spacing; avoid arbitrary pixel values unless necessary. - Use
size-*when height and width would be the same (h-*andw-*).
- Prefer Tailwind’s 12-column grid:
grid grid-cols-12on the container, thencol-span-*with breakpoint prefixes (sm:col-span-6,md:col-span-4, etc.) so columns reflow across breakpoints. - For three equal columns (e.g. profile grid, landing grid preview, post media thumbnails), use
grid-cols-12withcol-span-4on each cell (three × four = twelve).
// Prefer: semantic tokens, spacing scale, theme-aware surfaces
cva("flex items-center gap-4 p-4 bg-card rounded-lg border")
// Avoid: arbitrary pixels and ad-hoc light/dark when tokens exist
cva("flex items-center p-[17px] bg-white dark:bg-gray-800 rounded-[10px]")bg-scheduled,bg-draft,bg-published(and matchingtext-*variants).
- Prefer semantic HTML (
main,header,nav,button). - Add ARIA labels where needed; use
sr-onlyfor text meant only for screen readers. - Ensure keyboard navigation works inside modals and dialogs.