Architecture Defaults
These are the recurring architecture choices across your active repos.
They are not universal laws. They are the default answers when a new project needs these capabilities and there is no strong reason to do something else.
These defaults intentionally ignore PHP/Craft maintenance surfaces. The useful signal for new work is the direct TypeScript, UI, data, and agent dependency surface.
Full-Stack TypeScript App
For a serious TypeScript product app, the recurring stack is:
- Next.js 16.2 App Router
tRPCfor the typed API layer@tanstack/react-queryfor client data orchestration- Drizzle for the database layer
- Neon Postgres via
@neondatabase/serverless
This pattern shows up strongly in current full-stack product apps.
Default package set
@trpc/server@trpc/client@trpc/tanstack-react-query@tanstack/react-querydrizzle-ormdrizzle-kit@neondatabase/serverless
When this is the right default
Use this 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 real database-backed behavior, not just static pages
When not to use it
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 alternative. It keeps end-to-end TypeScript inference while producing an OpenAPI contract, so a non-TypeScript client is a first-class consumer. Reach for oRPC in that case rather than hand-rolling REST handlers.
Database and Persistence
The clear default is:
- Postgres
- Drizzle (ORM + typed schema)
- Neon for serverless-hosted Postgres
@neondatabase/serverlessas the driver — never rawpg/node-postgres, andpostgres.jsonly for a concrete long-running-service need
This is one of the strongest repeated patterns in the current portfolio. Use Drizzle from day one — hand-written SQL with manual row typing is not the baseline.
Connecting Drizzle to Neon (the rule)
Pick the adapter by runtime, not by habit. Both adapters use the same @neondatabase/serverless package:
- Default —
drizzle-orm/neon-http(neon(DATABASE_URL)→drizzle(sql, { schema })). Use for all app data access (Server Components, route handlers, serverless/edge functions) and short-lived scripts. HTTP one-shot queries are lowest-latency for request/response work, are edge-safe, anddb.transaction([...])still gives atomic batched (non-interactive) writes — which covers almost every write path, including multi-table ingestion. - Escape hatch —
drizzle-orm/neon-serverless(Poolover WebSockets; setneonConfig.poolQueryViaFetch = trueon edge). Use only when a runtime genuinely needs interactive (session) transactions — multi-step logic that branches mid-transaction — or node-postgres compatibility. Typically long-running workers/CLIs. Open and close the pool within the request/process.
Do not mix adapters within one package without a reason, and do not reach for postgres.js/node-postgres as a default — that is driver sprawl.
Schema and migrations
- Schema-first. The Drizzle schema (
packages/db/src/schema.ts) is the source of truth. drizzle-kit pushis the workflow for syncing schema to the database. Do not hand-author migration files or runtime DDL. For an existing database, bootstrap the schema once withdrizzle-kit pull, then own it via push.- Schema lives in the first-tier
packages/dbboundary.
Driver notes (GA @neondatabase/serverless ≥ 1.0)
- 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). - Over HTTP, wrap reads/writes in retry-on-transient-drop logic; connections can blip during maintenance.
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
- use Storybook as the visual contract
- seed new UI-first work from the bundled UI baseline
Storybook is not mandatory for every app. It is mandatory when the repo exports reusable UI that should be reviewed and regression-checked 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 as of July 2026, 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
files-sdkas the default object/blob-store abstraction inside storage packages when code needs to support S3, R2, GCS, Azure Blob, Vercel Blob, Netlify Blobs, MinIO, or similar backends through one API - 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, structured CLI output, and MCP tools. The storage platform owns durable storage and delivery. files-sdk owns the provider-neutral object/blob-store calls underneath a repo-local storage or upload boundary, not ad hoc provider clients in app routes.
AI-Enabled Apps
For apps that genuinely need AI features, the recurring pattern is:
ai(AI SDK v6) for the application-facing AI SDK surface@howells/aias the shared AI SDK/provider baselinehowells/motiffor fal.ai image-generation and media-utility surfaceszodfor structured input and output contracts
Default model access is the AI Gateway. On AI SDK v6 the global provider is the Vercel AI Gateway, so pass a "provider/model" string into AI SDK calls and requests route through the Gateway — one credential, provider switching without new SDK wiring. Per-provider @ai-sdk/* packages are the escape hatch for direct-provider needs, still behind @howells/ai. Keep the model-string boundary in @howells/ai or packages/ai, not scattered through app routes.
If the repo is doing CLI-model orchestration or needs stricter typed IO around agent calls:
- consider
@howells/envelope
When the repo is doing real agent orchestration, add Mastra deliberately:
@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 or a generic packages/agents boundary. See Agentic Development.
Use raw provider SDKs only behind a boundary:
- default provider wiring belongs in
@howells/ai - project-specific provider composition belongs in
packages/ai - 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
Recent agent-heavy repos are converging on explicit package boundaries for tool surfaces:
packages/aifor repo-specific model/provider composition above@howells/aipackages/mastrafor Mastra runtime code, agents, tools, workflows, storage, memory, observability, scorers, and runtime routespackages/agentsfor reusable non-Mastra agent definitions, evaluators, prompts, and tool wiringpackages/mcporpackages/mcp-serverfor MCP server contracts and transport codepackages/cliwhen the agent or ingestion surface needs a first-class command line
Use zod for tool and transport schemas, and keep provider plumbing behind @howells/ai instead of scattering raw AI SDK clients through app code.
Use @modelcontextprotocol/sdk when the repo exposes MCP tools or resources. Do not bury MCP tool contracts inside a Next.js route unless the route is the only consumer and there is no expected CLI, test, or agent reuse.
Ingestion and Enrichment
For source-heavy or scraper-heavy repos:
- use a dedicated
ingestion,enrichment, orscraperpackage when pipeline behavior becomes substantial - consider
@howells/srcfullfor browser/page-source extraction 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:
@howells/stacksheet - thumbnail-to-expanded image or video transitions:
@howells/aperto
This avoids the common mistake of stretching a simple drawer primitive into a multi-layer workflow it was not designed to own.
Short Version
For a new product app, the default answer is usually:
- Next.js
tRPC- React Query
- Drizzle
- Neon
- Clerk
- the bundled UI baseline
@howells/aiplus Mastra/MCP packages when agent behavior is part of the product- Envy if runtime env exists
- Storybook if shared UI exists
- Fumadocs if the repo needs docs
- the house media storage packages if the repo needs media storage
Only deviate when the product constraints actually justify it.
See also: