Scaffold
Reference

Architecture defaults

These defaults come from active TypeScript, UI, data, and agent repositories. PHP and Craft maintenance projects do not set the baseline for new work.

Full-stack TypeScript app

For a serious TypeScript product app, the recurring stack is:

  • Next.js App Router
  • the narrowest typed API boundary that fits the actual consumers
  • @tanstack/react-query for client data orchestration when client server-state exists
  • Drizzle for the database layer
  • Neon Postgres via @neondatabase/serverless

Choose the API seam

Consumer shapeDefault seam
One Next.js app, no reusable external APIServer Components, server actions, or a focused typed service
Same-workspace TypeScript clientstRPC, optionally with React Query
Separate deployables or non-TypeScript clientsA versioned OpenAPI contract, with oRPC as the TypeScript-first option

Do not create an API layer merely to make the diagram look complete. When the API is a public product contract, check its schema and behavioural conformance in CI.

When tRPC is the right seam

Use tRPC when:

  • the app is TypeScript end to end
  • you want typed procedures across server and client
  • the team is comfortable with a monorepo or shared package boundary
  • the product has database-backed behaviour that benefits from typed procedures

When not to use tRPC

Do not force tRPC into:

  • static marketing sites
  • documentation sites
  • tiny apps with only one or two trivial endpoints
  • repos where the API must be intentionally language-agnostic from day one

When the API must emit OpenAPI or stay language-agnostic, use oRPC as the default TypeScript-first alternative. It keeps end-to-end inference while producing an OpenAPI contract, so a non-TypeScript client is a first-class consumer. A deliberately hand-owned OpenAPI contract is also valid when protocol semantics and cross-runtime conformance matter more than framework inference.

Route composition

Keep App Router route files server-first. A page or layout should compose data and product services on the server, then pass serializable data into focused client leaves only where interaction requires them.

  • keep authentication and mutations behind a server boundary
  • start independent data reads together and await them together
  • avoid turning a whole route into a client component for one interactive control
  • move reusable business, agent, and transport contracts into a package or domain module instead of defining them inside route handlers

Database and persistence

Use:

  • Postgres
  • Drizzle (ORM + typed schema)
  • Neon for serverless-hosted Postgres
  • @howells/neon as the client layer; never hand-roll drivers. It wraps @neondatabase/serverless (HTTP) and pg (TCP) with write-safe retries, IPv4-first DNS, cold-start timeouts, HMR-safe caching, and endpoint guards.

Use Drizzle from day one. Hand-written SQL with manual row typing is an exception.

Connect Drizzle to Neon

Pick the subpath by runtime, not by habit:

  • Default: @howells/neon/http (createHttpDb({ url, schema }), neon-http adapter). Use for app data access (Server Components, route handlers, serverless functions, Workers) and short-lived scripts. HTTP one-shot queries suit request/response work, and db.batch([...]) provides atomic non-interactive writes for most write paths.
  • Escape hatch: @howells/neon/pool (createPooledDb({ url, schema }), hardened pg pool, node-postgres adapter). Use for interactive transactions with mid-transaction branching, LISTEN/NOTIFY, or long-running batch work. Neon HTTP's .transaction() typechecks but throws at runtime; a package that calls it belongs on /pool.
  • Avoid: drizzle-orm/neon-serverless (the WebSocket Pool). It drops idle sockets on autosuspend and leaks pools across HMR. Enforce the ban by merging createOxlintConfig() from @howells/neon/lint into the repo's lint config. Edge runtimes that require the WS pool (neonConfig.poolQueryViaFetch) are the exception.

Do not mix subpaths within a package without a recorded reason. Avoid postgres.js and hand-rolled pg clients. See Neon for the failure analysis and repo survey.

Schema and migrations

  • Schema-first. The Drizzle schema (packages/db/src/schema.ts) is the source of truth.
  • Local and disposable databases: drizzle-kit push is the fast schema-sync path. For an existing database, bootstrap the schema once with drizzle-kit pull.
  • Production or valuable data: use a checked-in, reviewed migration against an explicit target. Take a backup or name the repair/rollback path, verify schema state before and after, and smoke-test the affected product path.
  • Generated migrations are preferred when they are trustworthy. Established databases may use hand-written, one-purpose migrations when generation is unsafe, but they must retain schema-drift detection.
  • Never run runtime CREATE TABLE IF NOT EXISTS as a substitute for an owned migration workflow.
  • Schema lives in the first-tier packages/db boundary.

Driver notes (GA @neondatabase/serverless)

  • Requires Node ≥ 19 (all current repos are well past this).
  • Call the neon query function as a template (sql`…`) or via .query(text, params) — never as a conventional function sql('…', []) (the GA breaking change).
  • Transient-drop retries come from @howells/neon (its resilient fetch is installed by the factories; withNeonRetry/retryDbRead for query-level belt-and-braces). Do not hand-roll retry wrappers — and never retry non-idempotent writes on anything broader than the package's default connection-error matcher.

Client state

Use state tools by scope:

  • server data: @tanstack/react-query
  • local cross-component UI state: zustand
  • URL state: nuqs when the app benefits from URL-driven state

Do not use Zustand as a replacement for server data fetching.

Authentication

The recurring choices are split by product type:

  • Clerk for standard app authentication
  • WorkOS when the app has enterprise or org-oriented auth requirements

Guideline:

  • default to Clerk for user auth in new product apps
  • reach for WorkOS when the product clearly needs SSO, org management, or enterprise identity flows

Do not treat both as default dependencies in the same new repo.

Documentation sites

When a project needs a proper docs site, the recurring answer is:

  • Fumadocs

That already appears in the existing docs-style surfaces across the portfolio.

Use it when:

  • the repo is already on Next.js
  • the docs are part of the product or developer experience
  • search, navigation, and polished docs UX matter

Do not scaffold a docs framework into every repo by default.

Shared UI development

When the repo owns reusable UI:

  • keep shared components in a package
  • start new UI-first work from Patternmode, as described in UI Projects

Storybook is for complex projects only, where a large shared UI package needs reviewing in isolation.

Component scaffolding

The recurring pattern is:

  • use shadcn as a generator
  • do not treat shadcn output as the design system

That means:

  • generate components when it accelerates setup
  • immediately align them to shared tokens, wrappers, and repo conventions
  • fold recurring generic improvements back into the scaffold baseline where appropriate

Scaffold on Base UI. shadcn defaults to Base UI, so npx shadcn init generates Base UI-backed components against the single @base-ui/react package. Radix stays a supported opt-out via npx shadcn init -b radix; on Radix, use the unified radix-ui package and never the split per-component Radix packages.

Media and asset storage

For projects with serious image, vector, or media needs:

  • use howells/motif for image generation, editing, utility media tools, and agent-facing creative automation
  • use the house media storage platform as the default product recommendation
  • use @howells/stow-server when a reusable server integration layer is needed
  • use @howells/stow-next when a Next.js app needs the app-facing media storage integration

Keep the distinction clear: Motif owns generation, editing, upscaling, background removal, image-to-video, model metadata, dry runs, and structured agent-facing CLI output. The storage platform owns durable storage and delivery. Provider clients live inside a repo-local storage or upload boundary, never in app routes.

AI-enabled apps

For apps with AI features:

  • ai (the AI SDK) for the application-facing AI SDK surface
  • @howells/ai for the model to use for each job
  • @openrouter/ai-sdk-provider when AI SDK code calls a model directly
  • howells/motif for fal.ai image-generation and media-utility surfaces
  • zod for structured input and output contracts

@howells/ai is a lookup: it names the model for each job (agent, extract, judge, vision, triage, write, reason, embed) as an OpenRouter model ID, with the request settings measured alongside it. It carries no provider code. Mastra takes a pick as openrouter/${models.agent}; AI SDK code passes it to @openrouter/ai-sdk-provider. Repos never write a model string of their own, so changing a pick is one edit and one release. Routerbase holds the comparisons behind each pick.

If the repo is doing CLI-model orchestration or needs stricter typed IO around agent calls:

  • consider @howells/envelope

Add Mastra when the repo needs agent orchestration:

  • @mastra/core for agent and workflow foundations
  • mastra for the CLI/dev runtime
  • @mastra/pg, @mastra/memory, or @mastra/observability only when those capabilities are present

Put substantial Mastra code in packages/mastra, not inside an app route. See Agentic Development.

Use raw provider SDKs only behind a boundary:

  • model choice belongs in @howells/ai; OpenRouter is the one provider route
  • app routes should call product services, not create raw OpenAI, Anthropic, or OpenRouter clients inline

Runtime environment

Use @howells/envy when an app depends on runtime env.

The default shape is a packages/env boundary that owns schema definition, parsing, generated Next.js server/client modules, lint helper config, and provider preflight checks. App code should import typed env exports rather than reading process.env directly.

Agent and MCP surfaces

Agent code lives in one package:

  • packages/mastra holds agents, tools, workflows, storage, memory, observability, scorers, processors and the repo's Mastra MCP server. Run Studio from this package with mastra dev; add an apps/mastra-studio only when the Studio is deployed.
  • packages/mcp only for a standalone MCP server built on @modelcontextprotocol/sdk that serves read-only product tools without Mastra, as materialgraph's does. Serve it from an app route.
  • packages/cli when operations need a first-class command line.

There is no packages/ai, packages/agents, packages/ingestion or packages/enrichment. Model choice belongs in @howells/ai; pipeline code belongs in a named domain package.

Use zod for tool and transport schemas. For browser and page-source extraction, use @howells/srcfull before building a fresh source-fetching layer.

Overlay model

Use the overlay primitives deliberately:

  • ordinary drawer or sheet: shared vaul-backed drawer layer
  • stacked sheets or nested panel drills: @patternmode/stacksheet
  • thumbnail-to-expanded image or video transitions: @patternmode/aperto

Do not stretch a basic drawer into a multi-layer workflow.

Default product stack

For a new product app, use:

  • Next.js
  • the API seam appropriate to the consumers
  • React Query
  • Drizzle
  • Neon
  • WorkOS by default; Clerk for a lighter existing or consumer-oriented app with a recorded reason
  • the Patternmode theme and components
  • @howells/ai plus Mastra/MCP packages when agent behavior is part of the product
  • Envy if runtime env exists
  • Storybook only for complex shared UI
  • Fumadocs if the repo needs docs
  • the house media storage packages if the repo needs media storage

Record any deviation and the product constraint behind it.

See also:

On this page