Org Dashboard
Dashboard with auth, RBAC, admin panel, and example CRUD. Ships as a turborepo with a Next.js web app, a Node batch worker, and shared packages.
Composition
Apps:
web— Next.js (→ 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
Architecture
Turborepo structure
apps/
web/ # Next.js app
batch/ # Node worker
packages/
api/ # tRPC routers
auth/ # Better Auth config + types
db/ # Drizzle schema + client
ui/ # shadcn components
config/ # Shared tsconfig
scripts/
local-setup.ts # One-command local bootstrap (env, docker, schema, seed)
seed.ts # Idempotent, flag-driven database seederWeb app
src/
├── app/
│ ├── layout.tsx # Root layout with AppProviders + Toaster
│ ├── (auth)/
│ │ ├── layout.tsx # Auth guard (redirect if logged in)
│ │ └── login/
│ │ ├── page.tsx
│ │ └── login-form.tsx # Email/password login form
│ └── (dashboard)/
│ ├── layout.tsx # Sidebar + auth guard + session check
│ ├── page.tsx # Dashboard home (contact count)
│ ├── admin/
│ │ ├── users/
│ │ │ ├── page.tsx # User list (admin only)
│ │ │ └── [id]/
│ │ │ ├── page.tsx # User detail
│ │ │ └── user-detail.tsx # Edit user + generate password
│ │ └── roles/page.tsx # Read-only roles + permission matrix (admin only)
│ ├── contacts/
│ │ ├── page.tsx # Contact list
│ │ └── [id]/page.tsx # Contact detail + edit
│ └── profile/
│ ├── layout.tsx # Tab navigation + ViewTransition
│ ├── page.tsx # Redirect → /profile/account
│ ├── account/page.tsx # Account info + edit name
│ ├── security/page.tsx # Change password
│ ├── sessions/page.tsx # Active sessions + revoke
│ └── preferences/page.tsx # Theme switcher
├── components/
│ ├── admin/ # Create user dialog, user actions, user table
│ ├── contacts/ # Contact dialog, form, table
│ ├── navigation/ # Sidebar, header, breadcrumbs, nav-user dropdown
│ ├── profile/ # Account form, security form, session list, preferences, tab nav
│ ├── can.tsx # Permission gate component
│ └── query-boundary.tsx # Suspense + ErrorBoundary + QueryErrorResetBoundary
├── hooks/
│ └── use-permission.ts # Synchronous role permission check
├── lib/
│ └── constants.ts # Route definitions with required permissions
└── styles/
└── globals.css # Slide animations for ViewTransitionAPI package (packages/api)
src/
├── root.ts # App router (user, contact, session)
├── trpc.ts # tRPC init with auth context + headers
├── middleware/
│ └── rbac.ts # adminProcedure + permissionProcedure(resource, action)
└── router/
├── user.ts # me, create, list, getById, edit, generatePassword
├── contact.ts # list, getById, create, update, delete, count
└── session.ts # list, revoke, revokeOthersAuth package (packages/auth)
src/
├── auth.ts # Better Auth config (admin plugin + ac/roles, drizzle adapter)
├── auth-client.ts # Client with adminClient (ac/roles) + inferAdditionalFields
├── permissions.ts # Access-control catalog + admin/user/manager roles
├── types.ts # Session type via auth.$Infer.Session
└── password.ts # Cryptographic password generationWhat's included
Authentication
Email/password login with Better Auth. The admin plugin powers a custom access-control model (admin/user/manager), ban/unban, and session management. Auth layout redirects authenticated users away from login. Dashboard layout requires authentication and a valid role.
Role-based access control
A real, single-tenant RBAC built on the Better Auth admin plugin's access control. The catalog and roles are code-defined in packages/auth/src/permissions.ts:
admin— fulluser/sessionmanagement + allcontactactionsuser(default) — full CRUD oncontact, no user managementmanager—contact: ['read']only, a read-only example role
Enforcement is on both sides: the server gates data routes with permissionProcedure(resource, action) (tRPC), and the UI gates affordances with the <Can> component / usePermission hook plus permission-filtered navigation. Adding a role is a one-file edit in permissions.ts (a single roleDefinitions map). Admins can browse the roles and their permission matrix (read-only) at /admin/roles. See Auth & Permissions in the generated docs/agents/auth.md.
Admin panel
User list with clickable rows linking to detail pages. Creating a user never asks for a password — user.create generates one server-side and returns it once. User detail allows editing username, name, phone, and regenerating one-time passwords (visible once, must be copied). User actions include role assignment (admin/user/manager) and ban/unban.
CRUD example (Contacts)
Full create, read, update, delete for contacts. Contact form with validation (TanStack Form + Zod). Contact list with link to detail/edit page.
Profile with View Transitions
Route-based profile tabs using React 19 ViewTransition API with directional slide animations. Four sections:
- Account — user info (email, role, joined date) + edit display name
- Security — change password (revokes other sessions)
- Sessions — list active sessions with device/IP info, revoke individual or all others
- Preferences — theme switcher (light/dark/system)
QueryBoundary
Reusable component wrapping Suspense + ErrorBoundary + QueryErrorResetBoundary. Used throughout for tRPC prefetch error handling with retry UI.
Local setup
scripts/local-setup.ts is a one-command bootstrap. It copies every .env.example to .env, starts the database with docker compose up -d --wait, applies the schema, then seeds demo data:
bun run local-setupEach step is labelled and the script exits on the first failure.
Database seed
scripts/seed.ts is idempotent (safe to re-run) and flag-driven via node:util parseArgs:
| Flag | Effect |
|---|---|
--fixtures | Add demo data (extra users, posts, contacts) generated with faker |
--reset | Wipe existing data before seeding |
--force | Allow --reset against a non-local database |
--help | Show usage |
Core seeding always creates three accounts via Better Auth's admin API:
admin@example.com/password(role: admin)user@example.com/password(role: user)manager@example.com/password(role: manager, read-only)
The seed runs from the repo root, so db:seed loads apps/web/.env via --env-file to resolve DATABASE_URL and BETTER_AUTH_SECRET. A --reset against a non-local DATABASE_URL is refused unless --force is passed.
Warning: These credentials are for local development only. Change passwords or disable seed accounts before deploying to any shared environment.
Extra dependencies
| Package | Purpose |
|---|---|
lucide-react | Icons in navigation, forms, session list |
react-error-boundary | QueryBoundary error handling |
sonner | Toast notifications |
zod | Form validation schemas |
vaul | Drawer behind the responsive dialog (mobile) |
@faker-js/faker | Demo data for the seed fixtures (dev only) |
Root scripts
| Script | Command | Purpose |
|---|---|---|
local-setup | bun scripts/local-setup.ts | One-command local bootstrap (env, docker, schema, seed) |
db:push | turbo db:push | Push schema to database |
db:generate | turbo db:generate | Generate migration files |
db:migrate | turbo db:migrate | Apply migrations |
db:studio | turbo db:studio | Open Drizzle Studio |
db:seed | bun --env-file=apps/web/.env scripts/seed.ts | Seed core data (--fixtures for demo data) |
start | turbo start | Start production server |
CLI usage
bunx create-faster myproject \
--blueprint org-dashboard \
--linter biome \
--git \
--pm bunAgent context
This blueprint ships AGENTS.md + docs/agents/ guides (architecture, auth & permissions, data layer) so AI coding agents understand the project out of the box. See Agent Context.

