Create FasterCreate Faster
Business

Org Dashboard (TanStack Start)

Dashboard with auth, role-based access control (admin/user/manager), admin panel, and example CRUD on TanStack Start. Ships as a turborepo with a TanStack Start web app, a Node batch worker, and shared packages.

Presentation

The Org Dashboard (Next.js) starter with a TanStack Start web app instead of Next.js. You get the same product: email/password login, three roles (admin, user, manager) enforced on the server and in the UI, an admin panel, a contacts CRUD, a profile area, a Node batch worker, a one-command local setup, and an idempotent seed. Only the web app changes: routing, route guards, and the production server follow TanStack Start.

Composition

Apps:

Project addons:

Shared with the Next.js variant

The blueprint inherits the templates of Org Dashboard (Next.js) (META extends) and replaces only what is specific to the web framework. Everything else is the same, and documented on that page:

  • the api, auth, db, ui and config packages, the tRPC routers (user, contact, session) and the RBAC middleware (adminProcedure, permissionProcedure)
  • the roles and permission catalog in packages/auth/src/permissions.ts, with the admin, user and manager roles
  • the database schema, the Docker PostgreSQL setup, scripts/local-setup.ts and the seed (--fixtures, --reset, --force), with the same three accounts (admin@example.com, user@example.com, manager@example.com)
  • the batch Node worker
  • the pages: login, dashboard home, contacts list and detail, admin users list and detail, admin roles, and the four profile tabs (account, security, sessions, preferences)
  • the generated AGENTS.md and docs/agents/ guides (the TanStack Start variant documents its own guards in docs/agents/auth.md)

The contacts and admin pages render the same markup in both variants (shared partials), so the two apps look and behave alike.

What differs from the Next.js variant

Next.jsTanStack Start
RoutingApp Router, route groups (auth) and (dashboard)File routes in src/routes/, pathless layouts _auth and _dashboard
Auth guardrequireSession() in the layout and in each protected pagebeforeLoad of the _dashboard layout, once for every dashboard route
Admin and permission checksrequireAdmin() / requirePermission() in each pagebeforeLoad of the admin and contacts layout routes, once for their pages, and of the dashboard home route
Data prefetchServer component prefetch inside a HydrateClient boundary; a detail page awaits its record and calls notFound() for an unknown idRoute loader prefetch without awaiting, useSuspenseQuery in the component; a detail loader awaits its record and throws notFound() for an unknown id, with a pendingComponent skeleton meanwhile
Post-login redirectQuery parameter validated by safeRedirectPathredirectTo search parameter validated in validateSearch with the same safeRedirectPath
Production servernext startNitro server (.output/server/index.mjs) started with Node
Profile tabsDirectional slide animation with React ViewTransitionPlain tab navigation, no transition

Pathless layouts and guards

_dashboard.tsx and _auth.tsx add no segment to the URL. Both decide through one isSignedIn(session) check in src/lib/auth/guards.ts, so a session one layout accepts is never refused by the other and the two never redirect to each other in a loop:

  • _dashboard calls requireSession(location) in beforeLoad. Without a session (or without a role) it redirects to /login?redirectTo=<requested path>, and returns { session } into the route context, so every child route and component reads the session from there. beforeLoad hooks run parent first and every loader starts after them, so an anonymous visit redirects before any protected query starts.
  • _auth wraps the login page and redirects a signed-in user to /.
  • admin.tsx calls requireAdmin(context.session) once for every page under /admin: a member is redirected to /. The bare /admin answers 404 through admin/index.tsx.
  • contacts.tsx calls requirePermission(context.session, { contact: ['read'] }) once for every page under /contacts: a role without the permission gets the not-found page with a 404 status. The dashboard home (_dashboard/index.tsx) makes the same contact:read check in its own beforeLoad.

The server still enforces every permission: each tRPC procedure keeps its adminProcedure or permissionProcedure, so the route guards only decide what a page shows.

Validated redirect

The login route reads redirectTo in validateSearch and passes it through safeRedirectPath (src/lib/safe-redirect.ts): only an internal path survives, anything else falls back to /. The login form navigates to the validated destination and never to a raw query parameter.

Session on the server

getSession (src/lib/auth/session.ts) is a server function that returns the current session through a per-request cache shared with the tRPC context, so a request looks the session up once. Better Auth uses the tanstackStartCookies() plugin, built into the app's own instance in src/lib/auth/auth.ts.

Architecture

The turborepo layout (apps/web, apps/batch, packages/*, scripts/) and the api, auth and db packages are those of the Next.js variant. The tree below lists the files the blueprint ships for the web app.

src/
├── routes/
│   ├── __root.tsx                        # Root document: theme provider, Toaster, devtools, not-found component
│   ├── _auth.tsx                         # Pathless layout: redirect a signed-in user to /
│   ├── _auth/
│   │   └── login.tsx                     # Login page, redirectTo validated in validateSearch
│   ├── _dashboard.tsx                    # Pathless layout: sidebar, header, requireSession in beforeLoad
│   ├── _dashboard/
│   │   ├── index.tsx                     # Dashboard home (contact count), requires contact:read
│   │   ├── admin.tsx                     # Layout route: requireAdmin
│   │   ├── admin/
│   │   │   ├── index.tsx                 # Bare /admin answers 404
│   │   │   ├── roles.tsx                 # Page title, renders the roles page
│   │   │   └── users/
│   │   │       ├── index.tsx             # User list
│   │   │       └── $id.tsx               # User detail
│   │   ├── contacts.tsx                  # Layout route: requirePermission contact:read
│   │   ├── contacts/
│   │   │   ├── index.tsx                 # Contact list
│   │   │   └── $id.tsx                   # Contact detail + edit
│   │   ├── profile.tsx                   # Tab navigation layout
│   │   └── profile/
│   │       ├── index.tsx                 # Redirect → /profile/account
│   │       ├── account.tsx               # Account info + edit name
│   │       ├── security.tsx              # Change password
│   │       ├── sessions.tsx              # Active sessions + revoke
│   │       └── preferences.tsx           # Theme switcher
├── components/
│   ├── admin/                            # Create user dialog, user actions, user table, user detail, roles page (matrix)
│   ├── auth/login-form.tsx               # Email/password login form
│   ├── contacts/                         # Contact dialog, form, table, detail
│   ├── navigation/                       # Sidebar, header, nav-user dropdown
│   ├── profile/                          # Account form, security form, session list, preferences, tab nav
│   ├── can.tsx                           # Permission gate component
│   ├── not-found.tsx                     # Not-found component (404)
│   └── query-boundary.tsx                # Suspense + ErrorBoundary + QueryErrorResetBoundary
├── hooks/
│   └── use-permission.ts                 # Synchronous role permission check
├── lib/
│   ├── auth/
│   │   ├── guards.ts                     # isSignedIn, requireSession, requireAdmin, requirePermission
│   │   └── permission.ts                 # hasPermission (client-side role check, also used by the guards)
│   ├── constants.ts                      # Route definitions with required permissions
│   ├── page-title.ts                     # Page title helper
│   └── safe-redirect.ts                  # Validates post-login redirect paths

What's included

The features (login, role-based access control, admin panel, contacts CRUD, profile, local setup and seed) are those of the Next.js variant; What differs covers how the web app implements them. What is specific to this variant:

Production server

vite build emits a Nitro server to apps/web/.output/. start runs it with Node, loading .env when it exists, then .env.start (NODE_ENV=production and the default PORT). In the turborepo, bun run start runs turbo start. A client-address Nitro plugin (src/lib/nitro/client-ip.ts) fills x-forwarded-for with the connection address when a request has none, so Better Auth rate limits each client separately when the server is exposed directly.

Deployment

The blueprint supports the SST deployment: sst.config.ts deploys the web app with sst.aws.TanStackStart on an AWS Lambda Nitro preset, and each runtime secret (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL) becomes an sst.Secret.

The Cloudflare deployments are not available, and the CLI rejects them with this blueprint:

  • --deployment cloudflare: Better Auth uses a module-singleton database, while Cloudflare reaches PostgreSQL through a per-request binding (not yet supported).
  • --deployment cloudflare-static: Better Auth needs a server runtime, which a static deployment does not provide.

The Next.js variant is rejected the same way.

Extra dependencies

The blueprint adds the same packages as the Next.js variant, both built from the same META constants.

Environment variables

SEED_PASSWORD, as on the Next.js variant.

Root scripts

The root scripts are those of the Next.js variant.

CLI usage

bunx create-faster my-app \
  --blueprint org-dashboard-tanstack-start \
  --linter biome \
  --git \
  --pm bun

Agent context

This blueprint ships AGENTS.md + docs/agents/ guides (auth & permissions, data layer) so AI coding agents understand the project out of the box. See Agent Context.

On this page