GenerateSaaS

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

LayerTechLives in
FrontendNext.js 16 App Router + React 19apps/web
BackendHono 4 RPC@repo/api
AuthBetter Auth 1.6@repo/auth
DataDrizzle ORM 0.45 + Postgres@repo/database
JobsInngest 4@repo/runtime
CacheRedis (ioredis or @upstash/redis)@repo/runtime
UIshadcn/ui on Base UI + Tailwind CSS 4 + Phosphor icons, over a shared client logic layer@repo/ui, @repo/app-core
ValidationZod 4 + standard-schema@repo/api, app forms
Toolingpnpm 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
SurfaceFileExport
Browser RPC clientlib/api/client.tsapi
Server Components + Server Actionslib/api/server.tsgetServerApi() (forwards the session cookie)
Auth clientlib/auth-client.tsauthClient

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.

FileLocal edit a rewrite reverts
accordion.tsxHeight transition on the panel - registry keyframes read a CSS variable Base UI never sets
collapsible.tsxThe same height transition
navigation-menu.tsxCross-slide variants in data-[name=value] bracket form + a bg-popover/70 backdrop-blur-2xl popup surface
popover.tsxcollisionPadding forwarded to the positioner (its loss is a compile error at every call site) + the same glass surface
select.tsxalignItemWithTrigger={false} plus p-1 on the list
avatar.tsxrounded-[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.

On this page