Frontend conventions
Read this before you change code in frontend/.
Apply Module design to components and hooks. Hide interaction mechanics in controls and request, parsing and cache mechanics in data hooks; screens own product state and effects.
Tooling
Always use pnpm, never npm. Run commands from frontend/. Packages using
the generated API client require TypeScript 5.5 or newer and an ES2015-or-later
target for its Unicode regular expressions.
Legacy applications
webv2, admin and styleguide use the ui package. Never write custom
button or form styles:
- Buttons use
className="btn"with the variantsprimary,secondary,dangerandghost. - Form inputs such as
Input,SelectandCheckboxcome fromui/components/Form. - Shared components such as
Modal,FlashandNavbarcome fromui. - Wrap every table in
.table-container. It scrolls horizontally and shows edge shadows while content is off-screen in that direction. Keep the table and its rows transparent (translucent tints are fine) so the shadows show. When the table has no focusable content, give the wrapperrole="region",tabIndex={0}andaria-labelledby(oraria-label) so keyboard users can scroll it.
Forms
Legacy applications use react-hook-form. Paper applications use TanStack Form
with usePaperForm from paper-ui. Never keep form fields in plain useState.
Keep legacy forms on React Hook Form without bridging the two libraries.
Paper form controls compose through
usePaperForm, form.AppForm, form.Form and form.AppField; render the bound
control from the field, such as field.Input. Screens own default values,
TanStack validators and submission effects. required marks the native control;
add the corresponding rule to the field or form validators. Number inputs emit
numbers (an empty input emits NaN), checkboxes own booleans or string arrays,
and autocomplete controls retain their selected objects. Disabled controls keep
their form values; screen submission handlers decide which data to persist.
Use form.reset(values, { keepDefaultValues: true }) to load another draft
without replacing the hook's default values on the next render.
Paper consumers require TypeScript 5.4 or newer and the pinned TanStack Form peer.
For legacy applications:
- Set up form context with
useForm()and<FormProvider>. - Use
<Input>,<Select>and<TextArea>fromui; they read the form context throughuseFormContext(). - Wrap custom or non-standard inputs, such as the code editor, with
useController(). - Submit with
methods.handleSubmit()so built-in validation runs. - Use
methods.watch()for reactive values such as live previews, andmethods.reset()to load existing data.
Cross-app URLs
Use useAppUrls(), browserAppUrls() or appUrlsForHost() from
frontend/packages/ui/app-urls.tsx for home, account, admin and browser API
URLs. The account URL is always the account. sibling of the home URL. Resolve
them per request or browser call so branch hosts retain their sibling origins.
Supply AppUrlsProvider from each application's request properties for
consistent SSR and hydration. Avoid module-level runtime URL constants.
Production branch hosts use <branch>.preview.tadoku.app,
<branch>.account.preview.tadoku.app and
<branch>.admin.preview.tadoku.app. The resolver accepts one DNS label beneath
these reserved roots and preserves paths, ports and private server API endpoints.
Other production subdomains do not select a branch. Development keeps the
branch label directly beneath each configured development app host.
Server-side API calls use the private endpoint from
serverApiEndpointForHost() and forward the incoming x-dev-branch header,
which branch host routes set and production edge routes remove, so the
gateway signs the branch tenant. The resolver reads NEXT_SERVER_API_ENDPOINT
at request time, ahead of the built Next.js runtime config, so standalone
production servers honor their Deployment's private endpoint.
Authentication flows
Webv2 protected pages use useSessionOrRedirect() from
frontend/apps/webv2/app/common/session.ts. It waits for the client router and
the complete current location before sending an anonymous visitor to login.
The shared auth route helpers encode return_to so queries and fragments remain
part of the original destination. Registration uses the same protected-page hook.
Login, registration and settings links always open authv2. Webv2 and admin
read GET /identity/session on every page request, including guests, and
logout uses POST /identity/logout. frontend/packages/ui/identity.ts calls
both through the generated client; the API owns cookies. SSR forwards Cookie
and X-Dev-Branch to the private API endpoint, and responses use
Cache-Control: private, no-store. Session transport errors, invalid responses
and its bounded timeout produce a retry screen without authorizing protected
content. A protected query returning 401 stops without automatic reload or
retries; the visitor can retry explicitly. Logout failures show a message and
retain the current UI session until revocation succeeds. A fresh session read
on each navigation keeps session expiry current without sharing a snapshot
between tenants.
Webv3 reads GET /identity/session and the tenant role on the server,
forwarding only Tadoku session cookies to the request's public API gateway. Its
login link opens authv2 and upgrades the SSR path and search to the full browser
URL after hydration; without JavaScript fragments cannot be preserved. Logout is
an ordinary POST form that the server completes with POST /identity/logout.
Its mutation boundary requires the same Origin and same-origin Fetch Metadata,
and personalized responses disable shared caching.
Calling Tadoku API
Every frontend calls Tadoku API through the client generated from
services/tadoku-api/spec/openapi.yaml, whose operations validate responses with
Zod (ADR 003). Never write an API URL, request or response
type, or response schema by hand; see Tadoku API client.
Paper applications
webv3, paper-styleguide, paper-playground and paper-ui follow
Paper composition. Keep application services and
routers out of paper-ui. pnpm check:paper-boundaries rejects paper-ui in
legacy applications and ui, Next.js or Headless UI in Paper code.
Webv3 uses TanStack Start, Router and Query with request-scoped server rendering, Zod response validation and TanStack Form. Its data source and local development commands are documented in Webv3.
Checks
Vite tests of Next.js sources must transform JSX with the automatic runtime;
Next.js keeps jsx: preserve in its TypeScript configuration for its own build.
pnpm --filter <app> exec tsc --noEmit # typecheck (Paper apps: pnpm --filter <app> typecheck)
pnpm --filter <app> lint
pnpm --filter <app> test # when the app defines it
pnpm build # before creating a pull request