Folder structure

src/
  app/                          Next.js App Router — routing only
    (app)/                      end-user routes, wrapped by the app service layout
    (admin)/admin/              operator routes on a stable internal path
    layout.tsx                  root layout: providers, toaster, service worker
  services/
    .ai.md                      service rules and import rules
    api.ai.md · state.ai.md · page.ai.md
    design.md                   design tone — read before any screen work
    app/
      api/{domain}/             server actions      index.ts · types.ts · actions/*.ts
      state/{domain}/           domain state        index.ts · types.ts · model.ts · actions/*.ts
      page/{route}/             page components     index.tsx · *-section.tsx
      page/layout/              service layout      index.tsx (server) · client.tsx (hydration adapter)
    admin/
      .ai.md                    admin routes, account rules, entry guidance
      _components/              admin-only compositions (shell, page, stat cards)
      api/ · state/ · page/     same layers, separate domains
  lib/                          shared code
    components/ui/              comwit-ui components installed as source
    layout/                     root layout client (StateProvider, OverlayProvider, Toaster)
    state/                      ComwitProvider wiring and the AppContext type
    server-action/              typed server-function transport (one POST route handler)
    utils/                      cn · createAction · resolveActions · ActionError · http client
    popup.tsx · ui-text.ts · kst.ts · admin-routes.ts
  server/                       server-only
    config.ts                   ordinary server settings from process.env
    db/schema.ts                Drizzle schema (empty until a database is connected)
    repository/
      .ai.md                    repository rules
      types.ts                  PageRequest · Pageable
      schema.dbml               entity design — written before mock data
      _data/{entity}.ts         normalized mock rows
      {entity}.ts               CRUD per aggregate root
  proxy.ts                      request proxy: public admin prefix → internal /admin

src/app is routing only

Route files import a page component and return it. No 'use client' here except Next's own error boundaries, no data shaping, no layout logic beyond the service boundary. The home route is src/app/(app)/page.tsx; a root src/app/page.tsx is a build error by rule, because every route belongs to a service.

Each service has a layout pair. The route group's layout.tsx renders <Suspense fallback={null}> around the service's async layout; that layout awaits the basic user (getMe()) and hands the resolved value to a small client adapter that calls useUser.hydrate(...) before anything reads the user hook. Interactive UI and user subscriptions live in children of that adapter, never in it, so a sign-out cannot replay a stale server seed.

Detail pages have two options. Pass the id and let the client load:

export default async function Page({ params }) {
  const { id } = await params
  return <PostDetail id={id} />
}

Or resolve on the server when SEO matters, and hydrate the query:

export default async function Page({ params }) {
  const { id } = await params
  const post = await api.post.find(id)
  return <PostDetailRoute id={id} initialPost={post} />
}

src/services/{service}

One folder per kind of user. app and admin come with the template. Each holds the three client-facing layers with the same domain names inside:

LayerPathOwns
apiapi/{domain}/server actions shaped for the screen; talks to the repository
statestate/{domain}/the domain model, its queries and every side effect
pagepage/{route}/sections that read one domain hook and render

Rules:

  • No cross-imports between services. admin never imports from app.
  • Shared code goes to src/lib (client and universal) or src/server (server only).
  • External imports go through a domain's index.ts only. Importing api/post/actions/create from anywhere but api/post/index.ts is a lint error.
  • Relative imports are free inside one domain folder.
  • The api is called from state, or from a Server Component for hydration. Calling the api from a client component in useEffect is forbidden.

Domains in the api layer are page-fit, not table-fit. They group what a main screen needs, so when the screen changes the response can change with it. A domain may serve several pages; a page may read several domains.

src/server/repository

The single gateway to data. Repositories start as functions over normalized mock rows in _data/, joined on query like a real database would. When a database is connected only the inside of each function changes; the signatures, and therefore the api layer, stay.

  • import 'server-only' in every file.
  • One file per aggregate root (comments live inside post.ts).
  • findAll(filter, pageable), findById(id), create(data), update(id, data), delete(id); lists always return Pageable<T>.
  • IDs come from uuid() (UUID v7, time-ordered). Deletion is a soft delete; queries exclude deletedAt rows by default.
  • Raw values only. Frontend-friendly conversion belongs to the api layer.
  • schema.dbml is written first, before mock data, so relationships are designed rather than discovered.

src/lib and src/server

src/lib holds everything shared across services on the client side. The UI components are comwit-ui source installed by the CLI, so they are yours to edit, and the file header of each one is its API documentation. popup.tsx gives actions imperative confirm/alert/sheet dialogs; ui-text.ts holds every default string so the app's language is one file.

src/server is server-only: config.ts for ordinary settings, db/ for the Drizzle schema and client once connected, repository/ as above.

Write order

Features are written bottom-up and read top-down:

write:  repository → api → state → page
depend: page → state → api → repository

Inside a layer the order is fixed too. An api domain starts with types.ts (the interface and its Simple, Detail and Request shapes), then actions/*.ts, then index.ts. A state domain starts with types.ts (State and Actions), then model.ts, actions/*.ts, index.ts. A page starts with index.tsx composing sections that do not exist yet, then the sections.

What enforces it

The rules above are not conventions on a wiki. eslint-rules/ ships Oxlint plugins that fail pnpm run validate when a boundary is crossed:

RuleRejects
app-structure, no-root-app-folder, no-use-client-in-approutes outside src/app, a root page, client directives in route files
index-only-import, no-direct-api-action-importreaching into a domain past its index.ts
client-component-no-api-importa client component importing the api layer
api-structure, api-create-action, api-action-error, api-only-server-actionapi modules that skip resolveActions, createAction or ActionError
state-structure, no-direct-comwit-import, no-multiple-state-hook-callsstate folders missing their files, @comwit/state used outside the state layer, two hook calls for one domain
page-use-client, no-native-interactive, no-next-image, no-anchor-tagpage files without 'use client', raw <button>/<input>, next/image, <a> for internal links
no-db-in-api, server-only-import, drizzle-node-configdatabase access from the api layer, repository files without server-only, a Drizzle config that imports server modules
auth-hydration-boundary, better-auth-required-optionsa hydration adapter that also subscribes, Better Auth instances missing required options
kebab-case-filenamefile names that are not kebab-case