Project Structure
Understanding the Unstack Pro codebase organization
Understand the codebase so you can build your product on top. Auth, organizations, and admin are already organized and working.
You don't need to modify the auth system. Focus on the /app directory where you'll build your product features.
Root Structure
Almost everything is shared between the two editions:
├── app/ # Next.js App Router pages
├── actions/ # Server actions
├── components/ # Reusable React components
├── config/ # Configuration files
├── emails/ # Email templates (React Email)
├── lib/ # Utilities and core logic
├── patches/ # Bun patch files applied on install
├── public/ # Static assets
├── schemas/ # Validation schemas (Zod)
├── scripts/ # Build and utility scripts
├── styles/ # Global styles
├── types/ # TypeScript type definitions
├── video/ # Remotion demo-video project (own deps)
└── ... config filesThe difference is one directory and a couple of root files. Pick your edition with the switcher below — the choice is remembered across every page in these docs.
├── drizzle/ # SQL migrations + meta/_journal.json
├── drizzle.config.ts # Points drizzle-kit at lib/db/schema.ts
└── proxy.ts # Stamps the request path onto a headerThere is no convex/ directory. Better Auth runs inside Next.js, so everything
server-side lives under app/, actions/ and lib/.
Next 16 renamed middleware.ts to proxy.ts. This one stamps the current
request path onto an x-current-path header so Server Components can build
redirect targets that point back at the page the user was actually on.
└── convex/ # Convex functions, schema and Better AuthThere is no drizzle/, no drizzle.config.ts and no proxy.ts.
App Directory (/app)
Next.js 16 App Router structure with all routes.
Authentication Routes
app/auth/
├── layout.tsx # Auth pages layout
├── page.tsx # Auth redirect/landing
├── login/
│ ├── page.tsx
│ ├── loading.tsx
│ ├── 2fa/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── _components/
│ │ ├── backup-code-verify-form.tsx
│ │ ├── totp-verify-form.tsx
│ │ └── two-factor-form.tsx
│ └── _components/
│ ├── login-form.tsx
│ ├── otp-request-form.tsx
│ ├── passkey-login-button.tsx
│ └── password-login-form.tsx
├── register/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ ├── register-card.tsx
│ └── register-form.tsx
└── forgot-password/
├── page.tsx
├── loading.tsx
└── _components/
├── email-request-form.tsx
├── forgot-password-form.tsx
└── reset-password-form.tsxAccount Routes
app/account/
├── layout.tsx # Account layout with sidebar
├── page.tsx # Account dashboard
├── loading.tsx
├── settings/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ └── settings-client.tsx
├── security/
│ ├── page.tsx # Security overview
│ ├── sessions/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── _components/
│ │ ├── revoke-all-sessions-button.tsx
│ │ ├── revoke-other-sessions-button.tsx
│ │ ├── revoke-session-button.tsx
│ │ ├── session-card.tsx
│ │ └── sessions-client.tsx
│ ├── two-factor/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── _components/
│ │ ├── backup-codes-card.tsx
│ │ ├── backup-codes-step.tsx
│ │ ├── disable-two-factor.tsx
│ │ ├── enable-two-factor-form.tsx
│ │ ├── otp-verify-step.tsx
│ │ ├── qr-code-step.tsx
│ │ ├── regenerate-backup-codes.tsx
│ │ ├── sign-in-chain-card.tsx
│ │ └── view-backup-codes.tsx
│ ├── passkeys/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── _components/
│ │ ├── delete-passkey-dialog.tsx
│ │ ├── edit-passkey-name-dialog.tsx
│ │ ├── empty-state.tsx
│ │ ├── passkey-card.tsx
│ │ ├── passkeys-client-list.tsx
│ │ └── register-passkey-button.tsx
│ ├── audit/
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ └── _components/
│ │ ├── audit-card.tsx
│ │ └── audit-client.tsx
│ └── _components/
│ ├── delete-account-dialog.tsx
│ └── security-settings-client.tsx
├── connections/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ ├── connection-card.tsx
│ ├── connections-client.tsx
│ ├── empty-state.tsx
│ └── unlink-account-button.tsx
├── invitations/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ ├── invitation-highlighter.tsx
│ └── invitations-client.tsx
└── delete/
├── page.tsx
├── loading.tsx
└── _components/
└── delete-account-form.tsxOrganization Routes
app/organizations/
├── layout.tsx # Org layout
└── [organizationSlug]/
├── layout.tsx
├── members/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ └── members-table.tsx
├── invitations/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ ├── accept-invitation-button.tsx
│ ├── cancel-invitation-button.tsx
│ ├── invitation-card.tsx
│ ├── invitations-list.tsx
│ └── reject-invitation-button.tsx
├── teams/
│ ├── page.tsx
│ ├── loading.tsx
│ ├── [teamId]/
│ │ ├── page.tsx
│ │ └── loading.tsx
│ └── _components/
│ └── teams-list.tsx
├── roles/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ ├── create-role-dialog.tsx
│ ├── edit-role-dialog.tsx
│ ├── permissions-selector.tsx
│ ├── roles-manager.tsx
│ └── roles-table.tsx
├── settings/
│ ├── page.tsx
│ ├── loading.tsx
│ └── _components/
│ ├── delete-organization-dialog.tsx
│ ├── edit-organization-name.tsx
│ ├── organization-image-uploader.tsx
│ └── settings-client.tsx
└── billing/
├── page.tsx
├── loading.tsx
└── _components/
├── manage-billing.tsx
├── orders-table.tsx
├── plan-hero-card.tsx
└── upgrade-plan-button.tsxThe org list and the org dashboard sit at the same URLs in both editions, but they are filed differently:
app/organizations/
├── (list)/ # /organizations
│ ├── layout.tsx
│ ├── loading.tsx
│ └── page.tsx
└── [organizationSlug]/
└── (overview)/ # /organizations/[slug]
├── loading.tsx
└── page.tsxRoute groups keep each page's own layout and loading UI next to it without adding a path segment.
app/organizations/
├── loading.tsx # /organizations
├── page.tsx
└── [organizationSlug]/
├── loading.tsx # /organizations/[slug]
└── page.tsxAdmin Routes
app/admin/
├── layout.tsx # Admin shell
├── page.tsx # Admin dashboard
├── loading.tsx # Loading state
└── _components/
├── admin-sidebar.tsx
├── admin-stats.tsx
├── ban-user-dialog.tsx
├── banned-users-table.tsx
├── change-password-dialog.tsx
├── confirm-verify-email-dialog.tsx
├── create-user-dialog.tsx
├── revoke-sessions-dialog.tsx
├── set-role-dialog.tsx
├── user-summary.tsx
├── users-manager.tsx
└── users-table.tsxAPI Routes
app/api/
├── auth/
│ └── [...all]/
│ └── route.ts # Better Auth API handler
└── autumn/
└── [...all]/
└── route.ts # Autumn billing handlerOther Routes
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── providers.tsx # App providers
├── error.tsx # Route error boundary
├── global-error.tsx # Global error boundary
├── not-found.tsx # 404
├── forbidden/page.tsx # 403
├── robots.ts # generated
├── sitemap.ts # generated
├── opengraph-image.tsx # generated
├── twitter-image.tsx # generated
├── site.webmanifest
├── (legal)/ # Terms & Privacy scaffolding
│ ├── layout.tsx # Shared legal layout
│ ├── terms/page.tsx # Terms of Service (starter copy)
│ └── privacy/page.tsx # Privacy Policy (starter copy)
├── goodbye/
│ ├── page.tsx # Account deleted confirmation
│ └── loading.tsx
└── success/
├── page.tsx # Success page
└── loading.tsxThe app/(legal)/terms and app/(legal)/privacy pages ship with starter copy
parameterized via config/site.ts (appName, supportEmail). Replace and review
this copy before production — see the Customization guide.
Actions Directory (/actions)
Server actions for server-side operations. How much lives here is the single biggest architectural difference between the editions.
actions/
├── admin.ts # Admin panel mutations
├── organizations.ts # Organization, team, role and invitation mutations
└── users.ts # Account, session, 2FA and passkey mutationsEvery write goes through a server action. There are no live queries in this
edition: Server Components read through lib/server/*, and after a mutation the
client calls router.refresh() — or invalidates the relevant React Query key —
so the Server Component re-reads and the UI catches up. If a write succeeds but
the screen doesn't change, a missing refresh is the usual cause.
actions/
└── update-profile-picture.tsAlmost nothing is here because almost nothing needs to be. Writes are Convex
mutations called from the client, and reads are reactive useQuery subscriptions
that re-render on their own when the underlying data changes. Server Components
read through preloaded queries instead.
Server actions provide type-safe server-side functions callable from client components.
Components Directory (/components)
Reusable React components organized by category.
Auth Components
components/auth/
├── auth-footer-prompt.tsx
├── auth-link.tsx
├── authenticated-client.tsx
├── change-password-form.tsx
├── controlled-email-input.tsx
├── controlled-input.tsx
├── controlled-otp-input.tsx
├── controlled-password-input.tsx
├── editable-email-input.tsx
├── editable-name-input.tsx
├── form-submit-button.tsx
├── logout-button.tsx
├── otp-input.tsx
├── otp-sent-hint.tsx
├── otp-verify-form.tsx
├── password-input.tsx
├── profile-picture-uploader.tsx
├── profile-sidebar.tsx
├── reauthenticate-dialog.tsx
├── resend-control.tsx
└── verify-email-dialog.tsxOrganization Components
components/organization/
├── active-org-syncer.tsx
├── add-member-to-team-dialog.tsx
├── create-organization-dialog.tsx
├── create-team-dialog.tsx
├── invite-member-dialog.tsx
├── leave-organization-button.tsx
├── my-information-card.tsx
├── organization-selector.tsx
├── organization-sidebar.tsx
├── role-multiselect.tsx
├── team-card.tsx
├── team-members-empty-state.tsx
└── teams-empty-state.tsxUI Components
components/ui/
├── avatar.tsx
├── badge.tsx
├── button.tsx
├── card.tsx
├── checkbox.tsx
├── command.tsx
├── data-table.tsx
├── dialog.tsx
├── dropdown-menu.tsx
├── empty-state.tsx
├── field.tsx
├── form.tsx
├── input.tsx
├── label.tsx
├── popover.tsx
├── select.tsx
├── separator.tsx
├── sheet.tsx
├── sidebar.tsx
├── skeleton.tsx
├── spinner.tsx
├── table.tsx
├── textarea.tsx
├── toaster.tsx
├── tooltip.tsx
└── visually-hidden.tsxLanding Components
components/landing/
├── landing-nav.tsx # Landing page navigation
├── reveal.tsx # Scroll reveal animation
└── site-footer.tsx # Landing page footerOther Components
components/
├── danger-zone.tsx
├── datetime.tsx # Locale-stable date/time rendering
├── faq.tsx
├── json-ld.tsx
├── page-header.tsx
├── react-scan.tsx
├── setting-row.tsx
├── theme-toggle.tsx
└── user-dropdown.tsxConfig Directory (/config)
Application configuration files.
config/
├── fonts.ts # Font configuration
└── site.ts # Site metadata and constantsEmails Directory (/emails)
React Email templates for transactional emails.
emails/
├── delete-account-verification.tsx
├── index.ts # Template registry
├── organization-invitation-email.tsx
├── password-changed-email.tsx
├── reauth-otp-email.tsx
├── reset-password-email.tsx
├── signin-otp-email.tsx
└── verification-email.tsxWhich process renders and sends these differs by edition — Next.js in the
Postgres edition (lib/email.ts), Convex in the Convex edition
(convex/email.ts). The templates themselves are the same.
React Email lets you write email templates in React with great previews.
Lib Directory (/lib)
Core application logic and utilities.
Authentication
Shared by both editions:
lib/
├── audit-log.ts # Audit log helpers
├── auth-client.ts # Better Auth client config
├── autumn.ts # Autumn SDK integration
├── permissions.ts # RBAC permissions
├── session.ts # Session management
└── utils.ts # General utilitiesWhere Better Auth is configured, and what has to exist to reach it, is the part that differs:
lib/
├── auth.ts # The single betterAuth() instance
└── email.ts # Resend sender used by the auth callbackslib/auth.ts is the whole server configuration — plugins, hooks, rate limits —
and bun run auth:schema generates lib/db/auth-schema.ts from it.
lib/
├── auth-helpers.ts # Auth utility functions
├── auth-server.ts # Server-side Better Auth caller
├── autumn-server.ts # SDK-free Autumn REST client (safe for Convex)
├── http-polyfills.ts # HTTP polyfills the Convex runtime needs
└── use-preloaded-auth-query.ts # Preloaded authenticated query hookBetter Auth itself is configured in convex/betterAuth/auth.ts, not here — the
files above are the Next.js side of the bridge into it.
Data Layer
lib/
├── db/
│ ├── auth-schema.ts # generated by bun run auth:schema
│ ├── index.ts # Drizzle client, created with the full schema
│ └── schema.ts # Your tables; re-exports auth-schema
├── query-client.tsx # React Query client setup
└── server/
├── admin.ts
├── organizations.ts
├── user-cleanup.ts
└── users.tslib/server/* holds the reads. They are called from Server Components and never
from the browser, so there is no client-side data subscription to keep in sync —
see Actions Directory for how the UI updates after
a write.
lib/convex/
├── convex-provider.tsx # Convex React provider
└── query-client.tsx # Query client setupThe functions themselves live in convex/ at the repository root:
convex/
├── _generated/ # generated by convex dev — never edit
├── auth.config.ts # JWT provider config
├── betterAuth/
│ ├── admin.ts
│ ├── auth.ts # The Better Auth instance
│ ├── authSchema.ts # generated by bun scripts/generate-auth-schema.ts
│ ├── organizations.ts
│ ├── userCleanup.ts
│ ├── userValidation.ts
│ └── users.ts
├── convex.config.ts
├── email.ts # Resend sender
├── http.ts # Better Auth HTTP routes
└── schema.ts # Your tables, spread over the auth tablesPlugins
lib/plugin/
├── overrides/ # Custom auth overrides
│ ├── client.ts
│ ├── index.ts
│ └── server.ts
└── reauth/ # Step-up reauthentication plugin
├── client.ts
├── email-otp-gate.ts
├── index.ts
├── passkey-gate.ts
├── server.ts
├── shared.ts
└── two-factor-gate.tsThe reauth plugin gates sensitive actions behind a fresh proof of identity —
an emailed OTP, a passkey, or a TOTP code — depending on what the account has
enrolled.
Audit logging comes from the better-auth-audit package, with helpers in lib/audit-log.ts.
Hooks
lib/hooks/
├── use-debounced-save.ts # Debounced autosave
├── use-loading-callback.ts # Loading state management
├── use-mobile.ts # Mobile detection
└── use-resend-cooldown.ts # Resend-button cooldown timerUtilities
lib/utils/
├── admin.ts # Admin-related utilities
├── currency.ts # Currency formatting
├── date.ts # Date formatting
├── download.ts # File download utilities
├── organization.ts # Org management utilities
├── passkey-provider.ts # Passkey provider metadata
├── redirect.ts # Redirect-target helpers
├── roles.ts # Role helpers
├── string.ts # String utilities
├── toast.ts # Toast notifications
├── user-agent.ts # User agent parsing
└── user.ts # User display helpersSchemas Directory (/schemas)
Zod validation schemas.
schemas/
└── auth.ts # Auth form validation schemasScripts Directory (/scripts)
Build and utility scripts.
scripts/
├── capture-demo.ts # Playwright demo capture (bun run demo:capture)
├── compile-all-pages.ts # Page compilation script
├── debug-resend.ts # Resend delivery debugging
├── line-count.ts # Line counting utility
├── record-demo.ts # Demo recording script
├── run-demo-node.ts # Node runner for demo scripts
├── sync-template.ts # Template update sync script
└── view-file-tree.ts # File tree visualizationThe Convex edition adds scripts/generate-auth-schema.ts, which regenerates
convex/betterAuth/authSchema.ts. The Postgres edition does the equivalent with
the auth:schema package script instead.
scripts/sync-template.ts powers template updates (see
Template Updates); .template-version records the last
synced commit. The video/ project + demo:* scripts render a demo video —
see Customization.
Styles Directory (/styles)
Global CSS and Tailwind styles.
styles/
└── globals.css # Global styles and TailwindTypes Directory (/types)
TypeScript type definitions.
types/
└── organization.ts # Organization-related typesConfiguration Files
Root Config Files
├── .env.example # Example environment template
├── .env.local # Local environment (git-ignored)
├── .env.sentry-build-plugin # Sentry build configuration
├── .gitignore # Git ignore rules
├── .template-version # Last synced template commit
├── autumn.config.ts # Autumn configuration
├── biome.json # Biome linting/formatting
├── components.json # Shadcn/ui configuration
├── instrumentation-client.ts # Client-side instrumentation
├── instrumentation.ts # Server instrumentation
├── next-env.d.ts # Next.js TypeScript env
├── next.config.js # Next.js configuration
├── package.json # Dependencies and scripts
├── postcss.config.js # PostCSS configuration
├── sentry.edge.config.ts # Sentry Edge Runtime config
├── sentry.server.config.ts # Sentry server config
├── tailwind.config.js # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
└── vercel.json # Build command override — see DeploymentTwo more sit at the root in the Postgres edition only:
├── drizzle.config.ts # Schema path, migration output dir, DATABASE_URL
└── proxy.ts # Next 16's renamed middlewareNeither file exists here. The Convex deployment this repo talks to is selected by
CONVEX_DEPLOYMENT in .env.local, not by a root config file.
next.config.js
Next.js configuration with Sentry integration.
instrumentation.ts & instrumentation-client.ts
Sentry instrumentation for error tracking.
sentry.edge.config.ts
Sentry configuration for Edge Runtime functions.
sentry.server.config.ts
Sentry configuration for server-side error tracking.
tailwind.config.js
Tailwind CSS configuration with custom theme.
tsconfig.json
TypeScript configuration with path aliases.
Important aliases:
{
"@/*": "./*" # Root import alias
}biome.json
Biome configuration for linting and formatting.
package.json
Dependencies and scripts. These exist in both editions:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "tsgo --noEmit && next build --turbopack",
"start": "next start",
"lint": "biome lint .",
"format": "biome format --write .",
"demo:capture": "…",
"demo:video": "…",
"demo:render": "…"
}
}{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"auth:schema": "… @better-auth/cli generate --config lib/auth.ts --output lib/db/auth-schema.ts",
"test": "bun test",
"test:plugins": "bun test lib/plugin"
}
}db:push exists but must not be used to apply schema changes — it diffs the
schema straight against the database and skips the drizzle/ folder, so the
custom trigger migrations never run. Use db:generate then db:migrate.
There are no extra package scripts. The Convex CLI is invoked directly —
bun convex dev during development, bunx convex deploy from the build — and
the auth schema is regenerated with bun scripts/generate-auth-schema.ts.
File Conventions
Loading States
loading.tsx files provide loading UI:
// app/admin/loading.tsx
export default function Loading() {
return <Skeleton />
}Error Boundaries
error.tsx and global-error.tsx files handle errors:
// app/global-error.tsx
export default function GlobalError({ error, reset }) {
return <ErrorUI error={error} retry={reset} />
}Layouts
layout.tsx files wrap route segments:
// app/account/layout.tsx
export default function AccountLayout({ children }) {
return (
<div>
<Sidebar />
{children}
</div>
)
}Component Organization
Pattern: _components folders for route-specific components:
app/admin/
├── page.tsx
└── _components/ # Admin-only components
├── users-table.tsx
└── admin-stats.tsxUnderscore prefixed folders are not treated as routes by Next.js.
Key Technologies
Frontend
- Next.js 16: App Router, Server Components
- React 19: Latest React features
- Tailwind CSS: Utility-first styling
- Shadcn/ui: Component library
- React Hook Form: Form handling
- Zod: Schema validation
Backend
- Better Auth: Authentication framework
- Resend: Email service
- React Email: Email templates
- Autumn: Organization-scoped billing
Where Better Auth runs, and what it stores data in, is the edition difference:
- PostgreSQL: the database, reached with
pg(node-postgres) — no vendor-specific driver, so any provider works - Drizzle ORM: schema, queries and relations
- drizzle-kit: migration generation and application
- Better Auth runs inside Next.js, in
lib/auth.ts
- Convex: realtime database and backend
- kitcn: the Better Auth + Convex integration, imported
as
kitcn/auth - Better Auth runs inside Convex, in
convex/betterAuth/auth.ts
Development
- TypeScript: Type safety
- Biome: Fast linting/formatting
- Sentry: Error tracking (optional)
Adding New Features
New Route
- Create folder in
/app - Add
page.tsx - Add
layout.tsxif needed - Create
_componentsfor route-specific components
New Component
- Add to appropriate
/componentssubfolder - Export from component file
- Import where needed
- Keep reusable, not route-specific
New Data Operation
For a read: add a function to lib/server/, and call it from a Server
Component.
For a write:
- Create or extend a file in
/actions - Mark it with
'use server' - Export an async function that validates its input and checks the session
- Call it from a component, then
router.refresh()so the Server Component re-reads
- Create a file in
convex/ - Export a
query,mutationoractionwithargsvalidators - Read it from a component with
useQuery, or call it withuseMutation
Server actions in /actions are for the few operations that need Node APIs the
Convex runtime does not have.
New Email Template
- Create in
/emails - Use React Email components
- Add to email sending logic
- Test in development
Best Practices
- Co-location: Keep related code together
- Separation: Separate UI from logic
- Reusability: Extract reusable components
- Type Safety: Use TypeScript everywhere
- Server Components: Default to server, client when needed
- Actions: Use server actions for mutations
- Validation: Validate all inputs with Zod
- Error Handling: Handle errors gracefully
Development Tips
- Use
_componentsfor route-specific components - Keep
components/for truly reusable components - Server Components are default, add
'use client'when needed - Use path alias
@/for clean imports - Follow existing patterns for consistency