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-queryfor client data orchestration when client server-state exists- Drizzle for the database layer
- Neon Postgres via
@neondatabase/serverless
Choose the API seam
| Consumer shape | Default seam |
|---|---|
| One Next.js app, no reusable external API | Server Components, server actions, or a focused typed service |
| Same-workspace TypeScript clients | tRPC, optionally with React Query |
| Separate deployables or non-TypeScript clients | A 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/neonas the client layer; never hand-roll drivers. It wraps@neondatabase/serverless(HTTP) andpg(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, anddb.batch([...])provides atomic non-interactive writes for most write paths. - Escape hatch:
@howells/neon/pool(createPooledDb({ url, schema }), hardenedpgpool, 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 WebSocketPool). It drops idle sockets on autosuspend and leaks pools across HMR. Enforce the ban by mergingcreateOxlintConfig()from@howells/neon/lintinto 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 pushis the fast schema-sync path. For an existing database, bootstrap the schema once withdrizzle-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 EXISTSas a substitute for an owned migration workflow. - Schema lives in the first-tier
packages/dbboundary.
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 functionsql('…', [])(the GA breaking change). - Transient-drop retries come from
@howells/neon(its resilient fetch is installed by the factories;withNeonRetry/retryDbReadfor 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:
nuqswhen 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
shadcnas a generator - do not treat
shadcnoutput 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/motiffor 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-serverwhen a reusable server integration layer is needed - use
@howells/stow-nextwhen 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/aifor the model to use for each job@openrouter/ai-sdk-providerwhen AI SDK code calls a model directlyhowells/motiffor fal.ai image-generation and media-utility surfaceszodfor 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/corefor agent and workflow foundationsmastrafor the CLI/dev runtime@mastra/pg,@mastra/memory, or@mastra/observabilityonly 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/mastraholds agents, tools, workflows, storage, memory, observability, scorers, processors and the repo's Mastra MCP server. Run Studio from this package withmastra dev; add anapps/mastra-studioonly when the Studio is deployed.packages/mcponly for a standalone MCP server built on@modelcontextprotocol/sdkthat serves read-only product tools without Mastra, as materialgraph's does. Serve it from an app route.packages/cliwhen 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/aiplus 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: