Develop and extend
Guide

Develop the web app

Build Agent Barn UI features with Next.js App Router, feature-local Zod contracts, shared API transport, TanStack Query cache rules, and Playwright verification.

For
Frontend developers
On this page
  1. Overview
  2. Before you begin
  3. Understand the UI boundaries
  4. Run the web app
  5. Add a route
  6. Define the API contract
  7. Query server data
  8. Mutate server data
  9. Preserve authentication and Organization scope
  10. Build feature UI
  11. Handle loading and errors
  12. Work with streaming
  13. Test the feature
  14. Verify the change
  15. Troubleshooting
  16. Next steps

Agent Barn’s web app is a Next.js App Router application organized around feature boundaries, runtime-validated API contracts, and tenant-safe server-data caching.

A typical change moves through this path:

Change path
App Router page


Feature component


Query or mutation hook


Shared API client


Agent Barn Product API
  • Next.js App Router
  • React
  • TypeScript
  • TanStack Query
  • Zod
  • Tailwind CSS
  • shadcn / Radix
  • Zustand
  • Playwright

Overview

The current frontend stack also includes React Hook Form, Nuqs, and Sonner. Use ui/package.json and ui/pnpm-lock.yaml as the source of truth for exact dependency versions.

Three rules shape almost every change:

  • App Router pages compose features. Feature workflows live under ui/src/features/.
  • Important API responses are validated by a feature-local Zod schema, and frontend types are inferred from that schema.
  • Server data lives in TanStack Query, and every Organization-scoped query family is isolated across an Organization switch.

Before you begin

You need:

  • The agent-barn repository
  • Node.js 24
  • Corepack and the repository-pinned pnpm version
  • The UI dependencies installed
  • A running Product API
  • A local Agent Barn user
  • An Organization, when developing an Organization-scoped feature
  • Docker, when running the complete local stack

Complete local stack

The shortest path. Runs the databases, API, worker, and UI together.

Full stack
make setup
make run

UI-focused development

Run the backend in one terminal:

Backend terminal
make db-up
make migrate
make dev-api

Then the frontend in another:

Frontend terminal
make dev-ui

make dev-ui runs the Next.js development server from ui/. The UI is served at http://localhost:3000, and the Product API at http://localhost:8000.

Before changing a feature, read:

  • The relevant document under docs/features/
  • docs/architecture/ui.md
  • docs/guidelines/webapp.md
  • A neighboring feature with similar data and interaction requirements
  • Existing shared UI primitives
  • Related Playwright tests and data-support helpers

Understand the UI boundaries

The main frontend dependency flow is:

Dependency flow
Next.js App Router


Feature component


Feature query or mutation hook


Shared API singleton


Same-origin /api request


Next.js rewrite


Agent Barn Product API

Authentication, Organization selection, and query caching surround that flow:

Provider composition
NuqsAdapter
  └── QueryProvider
       └── TooltipProvider
            └── AppProvider
                 ├── Public auth route
                 └── UserContextProvider
                      └── OrganizationProvider
                           └── Protected application route

Reading the two diagrams together: a request starts at an App Router page, passes through a feature component and its query or mutation hook, reaches the shared API singleton, and leaves as a same-origin /api call that the Next rewrite forwards to the Product API. The provider stack wraps all of it — NuqsAdapter, then QueryProvider, then TooltipProvider, then AppProvider, which branches into public authentication routes on one side and UserContextProvider plus OrganizationProvider on the other.

Source responsibilities

Location Responsibility
ui/src/app/ App Router pages, layouts, route handlers, and route-level loading and error boundaries
ui/src/features/<feature>/ Domain-facing schemas, hooks, components, utilities, providers, and feature state
ui/src/auth/ Login, session state, current-user resolution, password flows, and protected-route behavior
ui/src/shared/ API transport, error normalization, query-key infrastructure, and root-level shared providers
ui/src/components/ Reusable application components and UI primitives
ui/src/dashboard/ Shared dashboard composition and shell components
ui/tests/ Playwright specs, page objects, API interception support, and fixtures

Feature shape

A feature normally earns this structure as it grows:

Feature directory
ui/src/features/<feature>/
├── schemas.ts
├── hooks/
│   ├── use-feature-query.ts
│   └── use-feature-actions.ts
├── components/
│   └── feature-panel.tsx
├── utils.ts
└── constants.ts

Do not create every optional file for a small feature. Add a boundary when it owns a real responsibility.

Use kebab-case filenames, PascalCase component names, use... hook names, @/* aliases for internal imports, and external imports before aliased and relative ones.

Run the web app

The UI sends browser requests to same-origin paths:

Same-origin paths
/api/v1/auth/me
/api/v1/organizations/{organization_id}/agents

The Next.js configuration rewrites /api/:path* to the backend configured by NEXT_PUBLIC_BACKEND_URL, whose local default is http://localhost:8000. You can set it explicitly:

Explicit backend URL
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000 make dev-ui

This arrangement lets the browser use same-origin /api URLs while Next.js resolves the backend location. The API singleton uses an empty base URL deliberately:

API singleton
const apiClient = createApiClient({ baseURL: "" });

Verify the development path

  • Open http://localhost:8000/api/v1/health
  • Confirm the response reports a connected database
  • Open http://localhost:3000/login and sign in with a local account
  • Confirm /dashboard redirects into an available Organization or Platform View
  • Check the browser network panel for same-origin /api/v1/... requests

Add a route

App Router pages are composition points. They choose the feature component and supply route parameters, but the feature workflow stays under ui/src/features/.

For an Organization-scoped resource detail page, use a structure such as:

Route and feature layout
ui/src/
├── app/
│   └── dashboard/
│       └── [orgId]/
│           └── resources/
│               └── [resourceId]/
│                   └── page.tsx
└── features/
    └── resources/
        ├── schemas.ts
        ├── hooks/
        ├── components/
        │   └── resource-detail-page.tsx
        └── utils.ts

The route itself should stay small, and remains a Server Component:

app/dashboard/[orgId]/resources/[resourceId]/page.tsx
import { ResourceDetailPage } from "@/features/resources/components/resource-detail-page";

interface PageProps {
  params: Promise<{ resourceId: string }>;
}

export default async function ResourceDetailRoute({ params }: PageProps) {
  const { resourceId } = await params;
  return <ResourceDetailPage resourceId={resourceId} />;
}

The feature component owns the client boundary:

features/resources/components/resource-detail-page.tsx
"use client";

import { useResource } from "../hooks/use-resource";

export function ResourceDetailPage({
  resourceId,
}: {
  resourceId: string;
}) {
  const { resource, isLoadingResource, error } = useResource(resourceId);

  // Render the feature state.
}

Choose the component boundary

Server Components

The default boundary

  • Route composition
  • Metadata
  • Static layout
  • Async route parameters
  • Components with no browser state or client hooks

Client Components

Add "use client" only when the component owns one of these

  • React state or effects
  • Event handlers
  • TanStack Query hooks
  • Zustand or Nuqs hooks
  • Browser APIs
  • Interactive Radix components

Do not mark an entire route tree as client-rendered because one nested control needs state. Place the client boundary at the smallest useful feature boundary.

Define the API contract

Important responses must be validated with a feature-local Zod schema. The shared client transforms the response before validation, so the schema describes the frontend shape.

Wire format

What the backend sends

Response
{
  "id": "7bc68dad-4f87-47fb-996c-636fc367103c",
  "display_name": "Example resource",
  "organization_id": "1a9650cf-dd44-4ca1-b105-c0cf19f62f56",
  "created_at": "2026-08-29T10:00:00Z"
}

Frontend format

What the schema validates

schemas.ts
import { z } from "zod";

export const ResourceSchema = z.object({
  id: z.string().uuid(),
  displayName: z.string(),
  organizationId: z.string().uuid(),
  createdAt: z.string(),
});

export type Resource = z.infer<typeof ResourceSchema>;

Export the schema and its inferred type from the same file.

Request transformation

Write ordinary request bodies in frontend camelCase:

Request
await api.patch(
  `${orgApiBase}/resources/${resourceId}`,
  {
    displayName: "Updated resource",
  },
);

The shared request interceptor sends:

Transmitted body
{
  "display_name": "Updated resource"
}

Do not pre-convert normal JSON request bodies to snake_case.

Query strings

The body transformer does not rewrite URL query-string names. Use the backend’s wire-format parameter names:

Query parameters
const params = new URLSearchParams();
params.set("page", String(page));
params.set("page_size", String(pageSize));
params.set("search", search);

Use page_size, not pageSize, in the URL unless the backend endpoint explicitly declares otherwise.

Schema validation

Supply the schema to the shared client:

Validated request
const response = await api.get<Resource>(
  `${orgApiBase}/resources/${resourceId}`,
  {
    schema: ResourceSchema,
  },
);

return response.data;

A contract mismatch then becomes an ApiError with a validation failure, instead of silently entering the UI as malformed data.

When an API response changes, update the backend response DTO, the feature Zod schema, query and mutation hooks, fixtures, intercepted API responses, Playwright expectations, and every component that renders the changed fields.

Query server data

Server-owned application data belongs in TanStack Query. Three pieces work together:

Query key

Built from the centralized factory in ui/src/shared/query-keys.ts, extended by feature-local helpers. Never a literal array scattered through components.

Feature hook

Owns the key, the enabled guard, the shared API call, and the response schema. Exposes domain-oriented names instead of a raw query observer.

Feature component

Consumes the hook and owns the loading, empty, and error presentation for the data it requested.

1. Define the query keys

utils.ts
import { createQueryKeyStructure } from "@/shared/query-keys";

export const resourcesKey = createQueryKeyStructure("resources");

The factory provides list and detail families:

Key families
resourcesKey.all
resourcesKey.lists()
resourcesKey.list({ filters })
resourcesKey.details()
resourcesKey.detail(resourceId)

Extend those helpers for nested resources:

Nested key
export const resourceHistoryKey = (resourceId: string) =>
  [...resourcesKey.detail(resourceId), "history"] as const;

Do not scatter literal arrays such as ["resources", "detail", id] through components.

2. Add an Organization-scoped hook

hooks/use-resource.ts
"use client";

import { useQuery } from "@tanstack/react-query";

import { useOrganizationApiBase } from "@/features/organizations/hooks/use-organization-api-base";
import { useOrganizationContext } from "@/features/organizations/providers/organization-provider";
import { api } from "@/shared/api";

import { ResourceSchema, type Resource } from "../schemas";
import { resourcesKey } from "../utils";

export function useResource(resourceId: string | null) {
  const orgApiBase = useOrganizationApiBase();
  const { selectedOrganization } = useOrganizationContext();
  const organizationId = selectedOrganization?.id ?? null;

  const query = useQuery({
    queryKey: resourcesKey.detail(
      organizationId && resourceId
        ? `${organizationId}:${resourceId}`
        : "none",
    ),
    enabled: organizationId !== null && resourceId !== null,
    queryFn: async () => {
      const response = await api.get<Resource>(
        `${orgApiBase}/resources/${resourceId}`,
        {
          schema: ResourceSchema,
        },
      );
      return response.data;
    },
  });

  return {
    resource: query.data ?? null,
    isLoadingResource: query.isPending,
    error: query.error,
    refetchResource: query.refetch,
  };
}

Hooks should expose domain-oriented names. Components should not need to understand every internal property of a raw query observer.

3. Decide how Organization identity enters the cache

Including Organization identity directly in the key makes the separation explicit, but you must still make sure components never display a previous Organization’s cached result as placeholder data.

4. Use progressive queries for load-more interfaces

Infinite query
const query = useInfiniteQuery({
  queryKey: resourcesKey.list({
    scope: { organizationId, mode: "infinite" },
    filters: { search, pageSize },
  }),
  queryFn: async ({ pageParam = 1 }) => {
    // Fetch one page.
  },
  initialPageParam: 1,
  getNextPageParam: (lastPage) => {
    const nextPage = lastPage.page + 1;
    const totalPages = Math.ceil(lastPage.total / lastPage.pageSize);
    return nextPage <= totalPages ? nextPage : undefined;
  },
});

Keep follow-up loading local. Loading another page should not replace the entire route with its initial loading fallback.

Mutate server data

Mutations own user-triggered server workflows. The normal lifecycle is:

  1. Submit A user action in an event handler starts the workflow — never an effect watching state.
  2. Validate Client-side validation runs first, usually a Zod form schema through React Hook Form.
  3. Mutate The TanStack mutation calls the shared API client, which transforms the body and validates the response.
  4. Update or invalidate Set the detail cache directly where the response is authoritative, then invalidate every affected family.
  5. Notify Report success or failure concisely, normally through Sonner.
  6. Render new state The refreshed cache re-renders the affected views without a manual refetch chain.

A representative update hook is:

hooks/use-update-resource.ts
"use client";

import { useMutation, useQueryClient } from "@tanstack/react-query";

import { useOrganizationApiBase } from "@/features/organizations/hooks/use-organization-api-base";
import { api } from "@/shared/api";

import { ResourceSchema, type Resource } from "../schemas";
import { resourcesKey } from "../utils";

type ResourceUpdate = {
  resourceId: string;
  displayName?: string;
};

export function useUpdateResource() {
  const queryClient = useQueryClient();
  const orgApiBase = useOrganizationApiBase();

  return useMutation({
    mutationFn: async ({ resourceId, ...body }: ResourceUpdate) => {
      const response = await api.patch<Resource>(
        `${orgApiBase}/resources/${resourceId}`,
        body,
        {
          schema: ResourceSchema,
        },
      );
      return response.data;
    },
    onSuccess: (resource) => {
      queryClient.setQueryData(
        resourcesKey.detail(resource.id),
        resource,
      );

      void queryClient.invalidateQueries({
        queryKey: resourcesKey.lists(),
      });
    },
  });
}

Adapt the cache key to the Organization strategy the feature selected.

Cache behavior by mutation

Mutation Cache behavior
Create Invalidate affected collection families
Update Update or invalidate the detail, and invalidate affected lists
Delete Remove the detail, and invalidate affected lists and summaries
Publish or restore Invalidate the current detail, version history, and relevant collections
Lifecycle action Refresh detail, lists, health, and other derived state affected by the transition

Do not invalidate only the component currently on screen. Consider every list, detail, summary, selector, and dashboard that represents the changed entity.

Preserve authentication and Organization scope

Protected application routes are composed through:

Protected route stack
AppProvider
  └── UserContextProvider
       └── OrganizationProvider
            └── Dashboard route

Authentication

The shared API client sends credentials, reads access tokens from the authentication store, attaches the bearer header, refreshes near-expiry tokens, coalesces simultaneous refresh attempts, retries one failed authenticated request after refresh, marks the session expired when refresh fails, and normalizes failures as ApiError.

Bypassing the shared client also bypasses token refresh, key transformation, schema validation, and normalized errors. Use skipAuth: true only for deliberately public endpoints — login, forgot password, reset password, invite acceptance, and best-effort logout cleanup.

Public authentication routes bypass UserContextProvider and OrganizationProvider. Protected routes resolve the current user before rendering their children.

Organization View

The active Organization comes from the URL, /dashboard/{orgId}/.... Use useOrganizationApiBase() to build Organization-scoped API paths:

Organization API base
const orgApiBase = useOrganizationApiBase();

const url = `${orgApiBase}/agents`;

That produces /api/v1/organizations/{organization_id}/agents.

Do not mutate a shared Organization header. The Organization is explicit in both the dashboard URL and the API URL. If the URL contains an inaccessible or stale Organization ID, OrganizationProvider redirects to an available fallback.

Platform View

Platform View lives under /dashboard/platform and has no active Organization. Platform hooks call /api/v1/platform/... directly and must not use useOrganizationApiBase().

Protect platform interfaces with the existing Platform Administrator UI boundary, but remember that the API remains the final authorization authority.

Organization switching and query isolation

Several existing query families do not encode Organization identity in their keys. On a genuine Organization-to-Organization switch, OrganizationProvider removes those known families before child query effects start.

Build feature UI

Build new interfaces from existing primitives and design tokens before introducing another abstraction.

State ownership

State Owner
API data TanStack Query
Form input React Hook Form or local form state
Open dialog or selected row Local component state
Shareable tab, search, or filter URL state through Nuqs, when appropriate
Current user Authentication context and query
Active Organization Organization provider and the URL
Remembered Organization fallback The existing Organization Zustand store
Theme or cross-feature client preference An existing provider, or an explicitly justified store

Use this, avoid this

Topic Use this Avoid this
Confirmation The shared ConfirmationDialog for deletion, publishing, discarding a draft, restoring a version, lifecycle changes, replacing access, and leaving with unsaved changes window.confirm(), window.alert(), or alert()
Option selection The shadcn Select primitives, with every SelectItem inside a SelectGroup A native <select> control for user-facing option selection
Server data TanStack Query as the single owner of freshness and invalidation Copying API responses into Zustand to make them globally reachable
Orchestration Event handlers, mutationFn, onSuccess, onError, or a domain-oriented action hook useEffect chains that watch mutation state and start the next workflow
Styling Tailwind utilities, the cn() helper, Agent Barn CSS variables, and the existing af-* classes Arbitrary page widths that shift dashboard content between routes
Transport The shared api singleton from @/shared/api An ad hoc fetch wrapper or a second Axios instance for normal API calls

Forms

For forms with meaningful validation: define a Zod form schema, infer the TypeScript form type, use React Hook Form with the Zod resolver, render accessible labels and field errors, submit through a mutation hook, disable duplicate submission while pending, and show concise success or failure feedback.

Confirmations

Use the shared ConfirmationDialog for every confirmation workflow:

ConfirmationDialog
<ConfirmationDialog
  open={isConfirmingDelete}
  onOpenChange={setIsConfirmingDelete}
  title="Delete this resource?"
  description="This action cannot be undone."
  confirmLabel="Delete resource"
  pendingLabel="Deleting…"
  variant="destructive"
  isPending={deleteMutation.isPending}
  onConfirm={handleDelete}
/>

Option selectors

Use the shared shadcn Select primitives, keeping every SelectItem inside a SelectGroup:

Select
<Select value={value} onValueChange={setValue}>
  <SelectTrigger>
    <SelectValue placeholder="Choose an option" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectItem value="first">First option</SelectItem>
      <SelectItem value="second">Second option</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Do not add a native <select> for user-facing option selection.

Styling

Use Tailwind utilities, the existing cn() helper, shadcn and Radix primitives, Lucide or existing Agent Barn icons, and Sonner for transient notifications. Reuse the semantic variables:

CSS variables
var(--bg)
var(--bg-elev)
var(--bg-soft)
var(--line)
var(--line-strong)
var(--ink)
var(--ink-3)
var(--accent-ink)
var(--ok)
var(--warn)
var(--err)

And the existing utility classes:

Utility classes
af-page
af-card
af-card-hover
af-btn
af-btn-primary
af-btn-danger
af-input

af-page owns dashboard page width and padding. Do not introduce arbitrary page widths that cause dashboard content to shift between routes.

Accessibility

Use semantic elements, real buttons for actions, labels connected to inputs, accessible dialog titles and descriptions, keyboard-operable controls, visible focus styles, meaningful empty and error states, text in addition to color for status, and accessible names that Playwright can select reliably.

Handle loading and errors

Choose the boundary according to who owns the asynchronous work.

Situation Boundary
The router owns an initial page-blocking wait Route-segment loading.tsx
The route cannot render after an initial failure Route-segment error.tsx
A feature component owns its initial query Local skeleton or loading state
A rendered component fails to load data Inline error with retry
Search, pagination, or refetch is pending Local follow-up loading state
One subtree is waiting Local Suspense boundary
User or Organization context is hydrating Provider-owned fallback

A TanStack Query hook error does not automatically reach an App Router error.tsx. If the component owns the query, it also owns the inline error and retry behavior.

Use the shared error UI where it fits:

AppErrorState
<AppErrorState
  error={error}
  title="We couldn't load the resources"
  description="The resource list is unavailable right now."
  onRetry={() => {
    void refetch();
  }}
  retryLabel="Retry"
/>

ApiError statuses

Status Suggested UI treatment
0 Network error with retry
401 Session-expiration flow
403 Access denied — explain that permission is required
404 Not-found or inaccessible state
409 Explain the conflicting business or lifecycle state
422 Show actionable form or validation feedback
500+ Server error with retry when safe
Zod validation failure Report a contract mismatch rather than rendering malformed data

Keep page content mounted during follow-up refetches where possible. A search or load-more operation should not replace a usable page with its first-load skeleton.

Work with streaming

Streaming path
Browser streaming fetch


Next.js route handler
/api/v1/organizations/{organizationId}/agents/{agentId}/logs/stream


Backend SSE endpoint


Agent log stream

The Next route handler runs in the Node.js runtime, forwards the bearer header, resolves the internal backend hostname server-side, preserves text/event-stream, disables response buffering and transformation, and forwards the upstream body as a stream.

The browser hook uses streaming fetch, supplies the bearer access token, reads the response through TextDecoderStream, parses SSE data: lines, aborts on unmount or disable, and reconnects with bounded exponential backoff.

Do not generalize this exception into another API client. For ordinary JSON requests, keep using the shared api singleton.

If another streaming feature is required, define its authentication behavior, proxy boundary, cancellation behavior, event framing, reconnect policy, error state, cache or local-state ownership, and browser test strategy.

Test the feature

UI behavior is verified with Playwright. Test responsibilities are divided across four locations:

ui/tests/e2e/ Specs

Describe user-visible behavior, arrange API state through shared data support, act through page objects, and hold every assertion.

ui/tests/pages/ Page objects

Expose user-level actions and centralize stable selectors. They contain no assertions.

ui/tests/pages/data-support/ Data support

Owns API interception, keeping route matching precise enough not to catch neighboring endpoints.

ui/tests/fixtures/ Fixtures

Own reusable static response data in the backend wire format.

Test ownership
ui/tests/e2e/
  Specs describe behavior and contain assertions


ui/tests/pages/
  Page objects own selectors and user actions


ui/tests/pages/data-support/
  Data support owns API interception


ui/tests/fixtures/
  Fixtures own reusable static response data

Selector priority

  1. Accessible role and name
  2. Label or stable visible text
  3. An existing test ID, when semantic selection is insufficient

Data support and fixtures

Keep request interception out of specs when a reusable feature support helper can own it. Mock responses must represent the backend wire format — the shared response interceptor then converts their snake_case fields to the frontend’s camelCase schemas. When inspecting a request body at the network boundary, expect the shared client to have already converted frontend fields to snake_case.

Keep endpoint matching precise. A broad wildcard can accidentally intercept list, detail, draft, version, and mutation endpoints with one response.

When an API schema changes, update fixture fields, response interception, request-body expectations, the feature Zod schema, affected page objects, and spec assertions together.

Run Playwright

From the repository root:

Browser tests
make test-ui

For headed or interactive debugging:

Debugging
cd ui
pnpm test:watch
pnpm test:ui
pnpm test:debug

The local Playwright server runs on port 3003 and uses a separate .next-e2e output directory, so it does not collide with a normal development server. Playwright retains traces on failure and captures failure screenshots.

Verify the change

Run the standard UI verification from the repository root:

UI verification
make lint-ui
make check-ui
make test-ui

These cover ESLint and Next.js rules, TypeScript type checking, and Playwright browser behavior.

When the change touches routing, a Next route handler, rewrites, environment-dependent behavior, or production composition, also run:

Production build
cd ui
pnpm build

When the feature changes an API contract, also run the relevant backend checks:

Backend checks
make check-api
make test-api

Before handing off the change, confirm:

  • Route files only compose feature behavior
  • Client boundaries are no broader than necessary
  • Important responses are validated by Zod
  • Frontend types are inferred from schemas
  • Ordinary calls use the shared API client
  • Query-string fields use backend names
  • Query keys are centralized
  • Organization identity or switch eviction protects cached data
  • Queries have enabled guards where required
  • Mutations update or invalidate all affected query families
  • Authentication and provider order remain intact
  • Platform View does not assume an active Organization
  • Loading and error ownership matches the asynchronous work
  • Confirmations use ConfirmationDialog
  • Option selectors use the shared shadcn Select
  • Server data remains in TanStack Query
  • Playwright selectors and interception are reusable
  • Lint, type checking, and relevant browser tests pass

Troubleshooting

Issue Likely cause What to check
Zod reports a missing camelCase field The backend response or mock does not match the expected wire contract Inspect the network response, the response transformation, and the feature schema
The API receives an unknown query parameter A frontend query string used pageSize instead of page_size Use backend wire names in URLSearchParams
A request carries no bearer token The feature bypassed the shared client, or used skipAuth incorrectly Import api from @/shared/api and inspect the auth interceptor
The app repeatedly refreshes or logs out Refresh handling was bypassed, or a public request was treated as protected Check the shared auth flow, the token response schema, and skipAuth usage
Data from the previous Organization flashes after switching The query key lacks Organization identity and is absent from switch eviction Dimension the key, or add the family to the Organization-scoped eviction set
An Organization-scoped hook fails in Platform View It called useOrganizationApiBase() with no active Organization Use a platform endpoint and a platform-specific hook
A query runs before an ID exists The query lacks an enabled guard Disable it until all required IDs and context are available
A successful mutation leaves stale UI The mutation invalidated the wrong key, or only one representation Invalidate affected list, detail, summary, and nested families
An App Router error page never appears The error belongs to a client query, not the router Render an inline component error with retry
The whole page flashes during pagination Follow-up loading reuses the initial route or page skeleton Keep the existing content and show local fetch-next-page feedback
A confirmation blocks Playwright or browser automation A native confirmation API was introduced Replace it with ConfirmationDialog
A selector is flaky The test depends on DOM structure or styling classes Prefer accessible roles, labels, or stable visible names
A Playwright mock handles the wrong endpoint The route wildcard is too broad Match method, path, and query behavior more precisely
An intercepted response fails only in the UI The mock used frontend camelCase instead of the backend wire format Return backend snake_case and let the API client transform it
Log streaming connects but no lines appear The stream was routed through buffered request handling Check the dedicated Next route handler and the SSE response headers
Development works but the production build fails A server/client boundary, environment variable, or route handler is invalid at build time Run pnpm build and inspect the first compile or prerender error
UI types pass but the real API fails Browser tests mock a contract the backend does not implement Run backend integration tests and compare OpenAPI with the Zod schema

Next steps

Documentation