Tech Stack
Every layer of the stack - Next.js 16, Hono RPC, Better Auth, Drizzle with Postgres, Inngest, and Redis - and the package each one lives in.
A Next.js App Router frontend on a fully typed Hono RPC backend, sharing one set of @repo/* packages. Versions are pinned in the repo's package.json files and the catalog: block of pnpm-workspace.yaml.
The stack at a glance
| Layer | Tech | Lives in |
|---|---|---|
| Frontend | Next.js 16 App Router + React 19 | apps/web |
| Backend | Hono 4 RPC | @repo/api |
| Auth | Better Auth 1.6 | @repo/auth |
| Data | Drizzle ORM 0.45 + Postgres | @repo/database |
| Jobs | Inngest 4 | @repo/runtime |
| Cache | Redis (ioredis or @upstash/redis) | @repo/runtime |
| UI | shadcn/ui on Base UI + Tailwind CSS 4 + Phosphor icons, over a shared client logic layer | @repo/ui, @repo/app-core |
| Validation | Zod 4 + standard-schema | @repo/api, app forms |
| Tooling | pnpm 11 + Turborepo 2 + TypeScript 6 (strict) | repo root |
There is no @repo/jobs or @repo/cache: Inngest and Redis both live in @repo/runtime, which the app pulls in transitively through @repo/api.
Type safety end to end
The same types flow from database to UI with no codegen step - a backend route change surfaces as a frontend type error.
// Drizzle schema -> inferred row types -> Hono route -> typed RPC client
import { hc } from "hono/client";
import type { AppType } from "@repo/api";
const api = hc<AppType>(process.env.NEXT_PUBLIC_API_URL!);
const res = await api.posts.$get(); // fully typed response, no manual DTOs| Surface | File | Export |
|---|---|---|
| Browser RPC client | lib/api/client.ts | api |
| Server Components + Server Actions | lib/api/server.ts | getServerApi() (forwards the session cookie) |
| Auth client | lib/auth-client.ts | authClient |
next-intl drives i18n and next-themes drives dark mode. The app imports backend logic only through @repo/*, never from a sibling app - see project structure.
Redis is not optional. It backs caching, the rate limiter, and the distributed mutex in every environment. Self-hosted uses ioredis and needs REDIS_URL; Upstash uses @upstash/redis and needs UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN - see environment variables.
Deploy targets
The Hono backend and every @repo/* package use web-standard APIs, so nothing ties you to one host.
- Host on Vercel, Docker, Fly.io, Railway, or any Node host - see deployment.
- The API runs inside the Next.js app by default; split it into a standalone server later with separate backend.
- Feature flags live in
@repo/config; env vars and the typed clients are wired around them.
Adding shadcn/ui components
components.json pins the base-vega style, so pnpm dlx shadcn add <component> fetches the Base UI build into @repo/ui matching what is already there. Leave that style value alone - changing it pulls a different primitive library into the same app.
"menuColor": "default-translucent" in components.json is what makes dropdown menus and selects frosted glass. The CLI rewrites the registry's markers at add time into a bg-popover/70 panel with a blurred before: layer; backdrop-blur-2xl (40px) is the house standard for every floating panel. Other values: default, inverted, inverted-translucent. Change it in every components.json at once.
Re-adding a component you already have reverts local edits. Six files in @repo/ui carry deliberate changes the registry does not ship. shadcn add prompts before replacing an existing file and --overwrite skips that prompt. Only popover.tsx breaks the build; the other five keep typecheck and tests green and surface as broken motion or layout in the browser.
| File | Local edit a rewrite reverts |
|---|---|
accordion.tsx | Height transition on the panel - registry keyframes read a CSS variable Base UI never sets |
collapsible.tsx | The same height transition |
navigation-menu.tsx | Cross-slide variants in data-[name=value] bracket form + a bg-popover/70 backdrop-blur-2xl popup surface |
popover.tsx | collisionPadding forwarded to the positioner (its loss is a compile error at every call site) + the same glass surface |
select.tsx | alignItemWithTrigger={false} plus p-1 on the list |
avatar.tsx | rounded-[inherit] on the image, the fallback, and the ring |
Preview any rewrite with pnpm dlx shadcn add <component> --diff, which prints the delta and writes nothing. form.tsx has no registry entry, so it can only be reconciled by hand.