New Route Scaffold — app/(core)/
This skill captures the exact workflow used to build the original /notes route (now app/(core)/notes, since rebuilt as a client workspace — see route-architecture.md). Follow it for every new authenticated route. Many decisions require information only Arman has — ask explicitly before assuming anything.
Companion files — read at the step that needs them
- Writing any scaffold file in Phases 1–4 → copy its template from file-templates.md. Every code template (types,
data.ts, route files, hydrators) lives there, keyed by phase.
- Route UI not built yet (placeholder
page.tsx) → read coming-soon-placeholder.md.
- Full shell implementations, data flow, anti-patterns, mobile, more questions for Arman → route-architecture.md.
- A route needs agent-aware context or bindings → invoke
surface-authoring; this includes agent-native routes, where outside helpers may receive an approved task-appropriate snapshot while native and resident agents remain isolated from ambient inheritance.
STOP: Questions to Ask Before Writing a Single Line
Never assume the following — always ask Arman:
- Layout shape — How many panels? Sidebar width? Resizable? What goes in the header area?
- View modes — Does this route have sub-views (like edit/preview/split)? What are they named?
- Group-by / filter dimensions — How is the sidebar list organized? What FK relationships drive grouping?
- Data fetch scope — What fields does the list view need? What triggers a "full" fetch?
- Redux slice — Does one already exist? Should we create one or extend an existing slice?
- Existing types — Do the feature types already exist? Are they derived from the DB or hand-written (likely stale)?
- Navigation entry — Does
features/shell/constants/nav-data.ts already have an entry? What favicon color/letter (constants/favicon-route-data.ts)?
- Mobile behavior — Any mobile-specific overrides beyond standard rules?
If anything is unclear, stop and ask. Do not invent layout details, field names, or group-by modes.
Phase 1: Audit & Fix Types
Before touching any route file, verify the feature type file matches types/database.types.ts.
Step 1 — Find the DB Row shape
# Search for the table definition in the generated types
grep -A 30 '"your_table": {' types/database.types.ts
Step 2 — Compare against the feature type file
Located at features/[feature]/types.ts. Hand-written types are almost always missing fields. The canonical pattern:
Template → copy the canonical features/[feature]/types.ts pattern (aliases, working interface, _CompatCheck guard, list projection, route enums) from file-templates.md § Phase 1.
Step 3 — Update Redux notes.types.ts factory functions
The createBlankNoteRecordFromPartial and createAutogeneratedNoteRecord functions hardcode default values for every field. After adding fields to the interface, update both factories to include the new fields — otherwise TypeScript will error. Key rule: new nullable fields default to null, not "" or [].
Phase 2: Server Data Layer
Create lib/[feature]/data.ts. This is the only place server-side DB queries live for this route.
Template → copy lib/[feature]/data.ts (list seed, single entity, preload helper) from file-templates.md § Phase 2.
Critical rules:
import "server-only" is mandatory — it prevents accidental client import
- Every function must be wrapped in React
cache() — layout, generateMetadata, and page all call the same function; cache() collapses them to one DB hit. 'use cache' is NOT available (cacheComponents off; build error)
- 🚨 Never
notFound() on an empty single-record read (authInterrupts is ON) — under RLS, empty means deleted, missing, denied, or signed out. The single-record read returns null; the route renders <AccessGate token id/> (live: app/(core)/pick-lists/[id]/page.tsx) or refuses with requireAccess(type, id, level, { forbid: true }). Read features/access-gate/FEATURE.md first
- The preload pattern starts a fetch before an await chain, eliminating waterfalls
Phase 3: Route Files
File map
app/(core)/[feature]/
├── layout.tsx ← static metadata only
├── loading.tsx ← dimension-exact skeleton of the full shell
├── error.tsx ← <ErrorBoundaryView context="[Feature]" /> one-liner
├── page.tsx ← fetches list seed, hydrates Redux, renders shell
└── [id]/
├── layout.tsx ← parallel fetch, generateMetadata, BOTH hydrators, shell
├── loading.tsx ← skeleton of the content area only (not the full shell)
├── error.tsx ← <ErrorBoundaryView context="[Feature] Detail" /> one-liner
├── not-found.tsx ← malformed-URL 404 only; an empty record read renders <AccessGate>
├── page.tsx ← redirect to default view
├── edit/layout.tsx ← titlePrefix sub-layout (Phase 6), one per view that needs its own tab title
├── edit/page.tsx
├── split/page.tsx
├── rich/page.tsx
├── md/page.tsx
├── preview/page.tsx
└── diff/page.tsx
error.tsx (route root and [id]/)
Every error boundary is a one-liner — never build error UI inline.
"use client";
import { ErrorBoundaryView } from "@/components/errors/ErrorBoundaryView";
export default function MyFeatureError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return <ErrorBoundaryView error={error} reset={reset} context="My Feature" />;
}
Optional props: context (string label for console logs), homePath (override home button path, default "/").
ErrorBoundaryView shows all users a polished error UI with retry/back/home actions. Admins get a collapsible debug panel with full error details, request context, user context, stack trace, raw JSON dump, and a "Copy for AI" button that strips minified chunk URLs and formats a clean Markdown summary for pasting into any AI chat.
page.tsx — placeholder for routes under development
Only when the route's real UI isn't built yet → read coming-soon-placeholder.md (ComingSoonPage usage and the four props you must always set).
layout.tsx (route root)
Static metadata only. No data fetching, no children manipulation.
Template → file-templates.md § Phase 3.
loading.tsx (route root)
Must mirror the exact dimensions of the shell. Every element has explicit h- and w-. No content-derived sizing. See route-architecture.md for the full skeleton example from the notes route.
Key rules:
- Outer container:
h-full flex overflow-hidden (AppShell .shell-main is already full viewport — do not subtract header height)
- Route chrome:
<PageHeader> — never an in-body faux header bar
- Sidebar:
w-[280px] shrink-0 — must match the real aside exactly
- No spinners —
<Skeleton> components only
[id]/loading.tsx mirrors the content area only (layout persists across navigation)
page.tsx (route root — index, no item selected)
Template → file-templates.md § Phase 3 (list seed + hydrator + Suspense + empty state).
[id]/layout.tsx — the most important file
This is where both hydrators live and parallel fetching happens.
Template → file-templates.md § Phase 3 (generateMetadata, preloadNote + Promise.all, <AccessGate> on an empty read, both hydrators, shell).
[id]/page.tsx — redirect to default view
Template → file-templates.md § Phase 3.
View sub-pages ([id]/edit/page.tsx, etc.)
Sub-pages export no metadata and fetch no data. A view that needs its own tab title gets a sibling layout.tsx with titlePrefix (Phase 6) — never a page-level { title }.
Templates → file-templates.md § Phase 3 (the titlePrefix sub-layout, the single-panel view, and the split view that passes both panels).
Phase 4: Redux Hydrators
Both live in features/[feature]/route/. They render null and dispatch synchronously during the first render pass — not in useEffect.
Templates → copy ListHydrator + EntityHydrator from file-templates.md § Phase 4.
Why useRef and NOT useEffect:
useEffect fires after paint — children reading from the store see empty state for one frame and flash. The useRef guard dispatches during the render pass, before any child reads.
Phase 5: Shell Components
All shell components are Server Components. Client Component islands are pushed as deep as possible — only the interactive parts get "use client".
Component tree:
[Feature]Shell (Server) — h-full flex overflow-hidden (+ PageHeader for route chrome)
├── [Feature]Sidebar (Server — w-[NNpx] shrink-0 frame) ← ask Arman for width
│ └── [Feature]SidebarClient (Client — Redux reads, interactions)
└── [Feature]MainArea (Server — flex-1 min-w-0 frame)
├── [Feature]TabBar (Client — open tabs from Redux)
└── {children} per page → [Feature]ViewShell (Server)
├── leftPanel (Client editor island)
└── rightPanel (Client preview island — split mode only)
Key layout rules:
- Outermost:
h-full overflow-hidden — never h-screen, h-page, or calc(100dvh - header) on (core) routes
- Route chrome:
<PageHeader> — see features/shell/components/header/variants/USAGE.md
- Sidebar:
w-[280px] shrink-0 — ask Arman for the exact width
- Main area:
flex-1 min-w-0 overflow-hidden
- Scroll regions:
overflow-y-auto only on the inner list container
- No inline styles unless Arman has explicitly approved (e.g., glass mode testing)
See route-architecture.md for the NotesShell / NoteViewShell composition — the pattern to build, not importable code (notes deleted its shell/ components 2026-06-24, features/notes/FEATURE.md).
Phase 6: Metadata Rules
From app/(core)/_read_first_route_rules/metadata-and-seo.md (full recipe: the route-metadata-favicons skill):
| Location | What to use | Notes |
|---|
layout.tsx (root) | createRouteMetadata("/path", { title, description }) | Provides favicon + OG for entire route |
[id]/layout.tsx | createDynamicRouteMetadata("/path", { title, description }) inside generateMetadata | Provides favicon + OG for item detail |
[id]/<view>/layout.tsx | createDynamicRouteMetadata("/path", { titlePrefix: "View Name", title: entity.name, letter }) (static route: createRouteMetadata + titlePrefix) | Tab reads View Name | Entity — AI Matrx. A page-level { title } renders View Name — AI Matrx and loses the name. No distinct title → no metadata; the parent layout covers favicon/OG. Live: agents/[id]/run/layout.tsx |
| New route | Add favicon entry in constants/favicon-route-data.ts | Ask Arman for color and letter |
Phase 7: Navigation Entry
Add to features/shell/constants/nav-data.ts (label, href, iconName, section, color) if not already present; the favicon hex + letter go in constants/favicon-route-data.ts. Ask Arman for all values — icon, href, section, color (Tailwind name + hex), and favicon letter. Never guess these.
Skeleton Rules (Non-Negotiable)
Every loading.tsx must satisfy:
- Identical outer container — same
h-, w-, flex classes as the real shell
- Exact fixed dimensions — every skeleton block has explicit
h- and w- (no content-derived sizing)
- Mirror structure — sidebar skeleton has toolbar row + list items; editor skeleton has title + body lines
- No spinners for page content —
<Skeleton> only (from @ai-matrx/design-system)
[id]/loading.tsx covers only the content area, not the full shell (the layout persists)
Checklist Before Asking Arman to Review
Related Skills — Read These First
These skills contain the rules this workflow is built on. When in doubt, they win:
ssr-zero-layout-shift — .claude/skills/ssr-zero-layout-shift/SKILL.md — server/client component boundaries, Suspense rules, hydration pattern, skeleton design, fixed-dimension containers, CLS prevention (absorbed nextjs-ssr-architecture + nextjs-app-router-expert)
- Shell header + page height —
features/shell/components/header/variants/USAGE.md — <PageHeader>, h-full, when .h-page applies
redux-selector-rules — .claude/skills/redux-selector-rules/SKILL.md — selector patterns, curried selector caching, avoiding re-render loops
core-route-headers — .claude/skills/core-route-headers/SKILL.md — invoke before writing any (core) shell or loading.tsx: <PageHeader> chrome, h-full overflow-hidden body, mobile header
- Route rules —
app/(core)/_read_first_route_rules/RULES.md — mandatory, read before every session on this route group
Additional Resources