Status: Active Derives from: ADR 0009 — Frontend Single App First. Public UI localization derives from ADR-0027.
TypeScript, React, and Next.js coding conventions. Frontend architecture (App Router layout, tenant resolution, SDK shape) is in 07-frontend-architecture.md.
- TypeScript with
"strict": trueand"noUncheckedIndexedAccess": true. - Target:
ES2022minimum. - Module resolution:
bundler. noImplicitAny,strictNullChecks,strictFunctionTypes,strictPropertyInitializationall on.verbatimModuleSyntax: trueto maketypeimports explicit.
ADR-0054 remediation pins
Node 22.23.1 in CI and frontend/.nvmrc; the workspace declares the same minimum.
Web CI and staged lint share scripts/lint-web.mjs inside the app: source, scripts
and root JS/TS configurations, including .mts/.cts. Generated next-env.d.ts,
.next and .server are excluded. Native ESM/CJS TypeScript helpers use an explicit
syntax parser outside the Next typecheck program. SDK/UI changes also run their
workspace lint in the hook. Generated native output is excluded from Prettier.
| Element | Convention |
|---|---|
| Files | kebab-case.ts(x) |
| React components | PascalCase |
| Component files | PascalCase.tsx |
| Hooks | useCamelCase |
| Types & interfaces | PascalCase (prefer type over interface) |
| Constants | UPPER_SNAKE_CASE for true constants; camelCase for derived |
| Enums | Avoid TS enums; prefer string literal unions or as const objects |
| Variables / functions | camelCase |
| Test files | *.test.ts(x) colocated |
- Use
import typefor type-only imports. - Group: 1) standard, 2) external, 3)
@learnstack/*packages, 4) local. Blank line between groups. - Absolute imports across features; relative within a feature.
- No circular imports — tested in CI.
- Prefer
typealiases overinterface(except when extending library interfaces). - Avoid
any. Useunknownand narrow. - Discriminated unions for state machines:
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: AppError };as constfor tuples and literal arrays.satisfiesfor shape checks without widening:
const ROLES = ["learner", "instructor", "tenant-admin"] as const satisfies readonly Role[];- Functional components only.
- Default to Server Components; mark
"use client"only when needed (interactivity, hooks, browser APIs). - Named exports; default re-export only when the route handler requires it.
- Props typed via a dedicated type, not inline.
type CourseCardProps = {
readonly course: PublicCourseSummary;
readonly onEnroll?: (courseId: CourseId) => void;
};
export function CourseCard({ course, onEnroll }: CourseCardProps) {
// ...
}- Custom hooks start with
use. - One hook per file under
src/.../hooks/. - Rules-of-hooks enforced by ESLint.
- Effect dependency overrides include a comment.
- Local state for view-only concerns.
- Server Components + URL search params for filterable lists.
useReducerfor complex client state.- TanStack Query for client-side server-state caching.
- Avoid global client stores unless multiple unrelated routes share the same mutable client state.
- Server Components call the typed SDK directly.
- Client Components route through server actions or RSC props; never call the API with bearer tokens directly.
- Cache keys carry the tenant, the organization where applicable, and the locale; the rule lives in Security Standards § Multi-Tenant + Organization Isolation Review Checklist.
- API errors mapped to typed
AppErrorbefore reaching UI code (see 09-error-handling.md).
- React Hook Form for non-trivial forms; controlled inputs for simple ones.
- Zod schemas for client-side validation; reuse on the server when possible.
- Inline error rendering; submit button stays enabled but the failure surface is accessible.
- Tailwind CSS for utility-first styling.
- Design tokens are the
--ls-*CSS custom properties that Frontend Architecture Standards § Tenant Branding names. Their defaults are declared inapps/web/src/app/globals.css, and the shared Tailwind preset inpackages/config/tailwindreads them.packages/uiholds no tokens, because ADR-0009 extracts a shared package only when duplication is real. Tenant theme overrides are applied at layout level. - No inline
style={{}}except for runtime-computed values (e.g. progress bar width). clsx/tailwind-mergefor conditional class composition.
P02d-2 accepts and implements G16's theme value grammar; P02d-4 delivers its public projection. Tenant Branding owns that contract. P02d-6 Step 1 implements G42's atomic server-rendered color injection; the delivery record owns verification and the remaining product/manual accessibility proof boundary.
- Live in
app/.../actions.ts. - Each action validates input with Zod before doing work.
- Each action returns a typed
Result<T, AppError>. - Server actions never read secrets from the client.
- Async functions return
Promise<T>and accept anAbortSignalwhen cancellable. - No
.then()chains in app code; useawait. - Handle errors with try/catch at boundaries; do not swallow.
- No
useEffectfor derived state; compute inline. - No
useEffectfor one-shot fetches in Server Components territory (the RSC does the fetch). - Effects pure: setup → cleanup; no side effects on every render.
dynamic(() => import(...), { ssr: false })for heavy, client-only components.- Image:
next/imagewith explicitwidth/height; never raw<img>for content images. - Font:
next/fontself-hosted. - Parallel fetches in RSC via
Promise.all; no waterfalls. - Memo (
useMemo,React.memo) only when profiled and proven to help.
- All interactive elements keyboard-reachable.
- Form inputs have associated labels.
- Headings follow document outline (no skipped levels).
- Color contrast meets WCAG 2.2 AA.
- See 16-accessibility.md.
console.erroronly via a centralizedloggerwrapper that ships to Sentry.- Never
alert(). Use toast or modal system. - Error boundaries at route-group level for graceful fallbacks.
Accepted G35 assigns the browser logger, Sentry and web-vitals to Phase 11; P02d-5 provides the bounded server caller and trace propagation, with no browser observability hook. See Frontend Observability. P02d-6 Step 1 implements public error/loading placement and localized state views. Standards 07 owns the approved status-route behavior.
P02d-6 G39 foundation delivered — Step 1, 2026-10-10.
ADR-0027 selects server-first next-intl
4.14.9 and apps/web/src/i18n/messages/{en,tr}/public.json. General UI identifiers
use lowercase dotted feature namespaces with snake_case segments, for example
public.catalog.course_count. Backend lockey_* keys retain their separate wire
contract; the SDK owns no translated resources.
Use ICU MessageFormat and plain text parameters. Do not use rich-text callbacks, HTML messages, authored URL attributes or client input as a lookup identifier. Require nonempty catalogues, equal key sets, valid ICU and matching argument names/types, plus checked callsite coverage. Missing required keys fail the build; formatter failures select a bounded translated unavailable state. Planted controls must prove these checks can fail. See Localization Standards for request integration and UI/content-language separation.
anywithout a comment explaining why.// @ts-ignore— use@ts-expect-errorwith a comment if absolutely necessary.- Direct
fetchfrom Client Components. - Direct
localStorage/sessionStorageoutside aclientStoragewrapper. - React class components.
- Default exports for components consumed across modules.
- Mutating props.
dangerouslySetInnerHTMLwithout a sanitization wrapper.
app/
(public)/
page.tsx
components/
hooks/
lib/
(studio)/
layout.tsx
page.tsx
...
(portal)/
...
packages/
ui/ # design system primitives
sdk/ # generated API client
config/ # shared configs
Feature folder layout:
features/<feature>/
components/ # presentational
hooks/ # behavior
lib/ # pure helpers
actions.ts # server actions
schemas.ts # zod schemas
types.ts
P02d-6 placement — Accepted 2026-10-09. Step 1 implements G39 in
apps/web/src/i18n/request.ts and the app-local message home above; no
packages/i18n is created. G41 uses synchronous public views under
apps/web/src/components/public/ for ordered plain-string default-card fields
only. Richer primitives remain Phase 04/05 and Phase 06. G31's delivered SDK
contract remains governed by Standards 07.
- Comment the why of non-obvious code, not the what.
- Don't restate JSX.
- Public component APIs have JSDoc.