Tech Stack
Explore the GenerateSaaS tech stack, from Next.js 16 and Hono RPC to Better Auth, Drizzle ORM with Postgres, Inngest, and Redis, fully typed end to end.
GenerateSaaS pairs a Next.js App Router frontend with a fully typed Hono RPC backend, sharing one set of @repo/* packages. Every layer is picked for type safety, deploy portability, and a boilerplate you fully own.
The stack at a glance
Each layer maps to a workspace package or app dependency, with the version pinned in the repo.
| Layer | Tech | Lives in | Why |
|---|---|---|---|
| Frontend | Next.js 16 App Router + React 19 | apps/web-next | Server Components by default, client only where interactive; prerenders marketing routes |
| Backend | Hono 4 RPC | @repo/api | End-to-end typed client via hc<AppType>(); deploys anywhere, not just Vercel |
| Auth | Better Auth 1.6 | @repo/auth | Sessions, OAuth, 2FA, passkeys, orgs, API keys - no third-party auth vendor |
| Data | Drizzle ORM 0.45 + Postgres | @repo/database | Typed schema and queries; SQL you can read, migrations you control |
| Jobs | Inngest 4 | @repo/runtime | Durable background/scheduled jobs with retries; no separate queue infra |
| UI | shadcn/ui (@base-ui/react) + Tailwind CSS 4 + Phosphor icons | @repo/ui-next | Accessible primitives you copy into the repo and own, in a shared package the app consumes; pnpm dlx shadcn add writes here; utility-first styling |
| Cache | Redis (ioredis or @upstash/redis) | @repo/runtime | Caching, rate-limit store, and distributed mutex; the environment variables depend on your cache choice |
| Validation | Zod 4 + standard-schema | @repo/api, app forms | Shared schemas validate API input via sValidator and forms via react-hook-form |
| Tooling | pnpm 11 + Turborepo 2 + TypeScript 6 | repo root | Fast workspace installs, cached task graph, strict types everywhere |
There is no @repo/jobs or @repo/cache package - Inngest and Redis both live in @repo/runtime, which the app pulls in transitively through @repo/api.
How the pieces connect
- The Next.js app imports backend logic only through
@repo/*packages - never from a sibling app. - It builds a typed RPC client with
hc<AppType>(), so a backend route change surfaces as a frontend type error. - Server Components read data through the server client; Server Actions and the browser client handle mutations and client reads.
- next-intl drives i18n and next-themes drives dark mode.
Type safety end to end
The same types flow from database to UI with no codegen step.
// 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 DTOsThe browser client lives in lib/api/client.ts; Server Components and Server Actions use getServerApi() from lib/api/server.ts, which forwards the session cookie.
Frequently asked questions
Do I have to deploy on Vercel?
No. The Hono backend and @repo/* packages use web-standard APIs, so you can host on Vercel, Docker, Fly.io, Railway, or any Node host. See deployment.
Is there a separate API server? By default the Hono API runs inside the Next.js app. You can split it into a standalone backend later - see separate backend.
Is Redis optional?
No. Redis backs caching, the rate limiter, and the distributed mutex in every environment. The self-hosted choice uses ioredis and requires REDIS_URL; the Upstash choice uses @upstash/redis and requires UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN - see environment variables.
Where do I configure which features are on?
Feature flags live in @repo/config; the typed client and env vars are wired around them. Start with configuration.
Can I still add more shadcn/ui components?
Yes. Every components.json pins the base-vega style, so pnpm dlx shadcn add <component> fetches the Base UI build and it drops into @repo/ui-next matching the components already there. Leave that style value alone - changing it pulls a different primitive library into the same app.
Why are dropdown menus and selects frosted glass?
Because components.json sets "menuColor": "default-translucent". The vega registry ships menu components with cn-menu-target and cn-menu-translucent markers, and the CLI rewrites those markers at add time into a real surface based on this setting - a bg-popover/70 panel with a blurred before: layer. The markers are build-time only: they are never CSS classes, and if you ever see one survive into a component file, that file was written without the transform running and the panel will render flat. The other values are default, inverted, and inverted-translucent. Change it in every components.json at once, or newly added menus will not match the ones you already have.
The blur strength that setting produces - backdrop-blur-2xl, 40px - is the house standard for every floating panel and for the navbar chrome. The registry only reaches dropdown menus and selects, so popover.tsx, navigation-menu.tsx, the navbar and the bottom nav carry the same surface by hand. Match it on anything new you float over the page.
Re-adding a component you already have is the risky case. Six files in @repo/ui-next carry deliberate edits the registry does not ship. shadcn add prompts before it replaces an existing file and --overwrite skips that prompt, so a rewrite silently reverts them. 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 | What a rewrite costs |
|---|---|---|
accordion.tsx | Height transition on the panel | Panels stop animating in both directions - the registry keyframes read a CSS variable Base UI never sets |
collapsible.tsx | The same height transition | Every collapsible opens and closes instantly |
navigation-menu.tsx | Cross-slide variants written in data-[name=value] bracket form, plus a bg-popover/70 backdrop-blur-2xl popup surface whose ring fades in with the panel | Those four classes compile to no CSS, so panel content stops sliding as you move between menu items, and the panel loses its glass surface and grows a heavy doubled top border while it scales open |
popover.tsx | collisionPadding forwarded to the positioner, plus a bg-popover/70 backdrop-blur-2xl popup surface | The prop leaves the component's types and every call site passing it fails to compile, and the panel reverts to an opaque card that no longer matches the menus |
select.tsx | alignItemWithTrigger={false} plus p-1 on the list | Popups cover their trigger instead of dropping below it, and items sit flush against the popup edge |
avatar.tsx | rounded-[inherit] on the image, the fallback, and the ring | A square avatar renders a circular image inside a square chip, with the ring cutting across the corners |
Preview any rewrite with pnpm dlx shadcn add <component> --diff, which prints the delta and writes nothing. form.tsx has no registry entry at all, so it can only ever be reconciled by hand.