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:
web— TanStack Start (→ docs) + shadcn/ui (→ module), Better Auth (→ module), tRPC (→ module), TanStack Query (→ module), TanStack Devtools (→ module), TanStack Form (→ module), Next Themes (→ module)batch— Node (→ docs)
Project addons:
- Database: → PostgreSQL
- ORM: → Drizzle
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,uiandconfigpackages, the tRPC routers (user,contact,session) and the RBAC middleware (adminProcedure,permissionProcedure) - the roles and permission catalog in
packages/auth/src/permissions.ts, with theadmin,userandmanagerroles - the database schema, the Docker PostgreSQL setup,
scripts/local-setup.tsand the seed (--fixtures,--reset,--force), with the same three accounts (admin@example.com,user@example.com,manager@example.com) - the
batchNode 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.mdanddocs/agents/guides (the TanStack Start variant documents its own guards indocs/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.js | TanStack Start | |
|---|---|---|
| Routing | App Router, route groups (auth) and (dashboard) | File routes in src/routes/, pathless layouts _auth and _dashboard |
| Auth guard | requireSession() in the layout and in each protected page | beforeLoad of the _dashboard layout, once for every dashboard route |
| Admin and permission checks | requireAdmin() / requirePermission() in each page | beforeLoad of the admin and contacts layout routes, once for their pages, and of the dashboard home route |
| Data prefetch | Server component prefetch inside a HydrateClient boundary; a detail page awaits its record and calls notFound() for an unknown id | Route 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 redirect | Query parameter validated by safeRedirectPath | redirectTo search parameter validated in validateSearch with the same safeRedirectPath |
| Production server | next start | Nitro server (.output/server/index.mjs) started with Node |
| Profile tabs | Directional slide animation with React ViewTransition | Plain 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:
_dashboardcallsrequireSession(location)inbeforeLoad. 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.beforeLoadhooks run parent first and every loader starts after them, so an anonymous visit redirects before any protected query starts._authwraps the login page and redirects a signed-in user to/.admin.tsxcallsrequireAdmin(context.session)once for every page under/admin: a member is redirected to/. The bare/adminanswers 404 throughadmin/index.tsx.contacts.tsxcallsrequirePermission(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 samecontact:readcheck in its ownbeforeLoad.
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 pathsWhat'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 bunAgent 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.

