Scheduling • Members • Payments • Analytics • Member Portal
Kudeak is a multi-tenant SaaS platform that gives gym owners everything they need to run their business — class scheduling, member management, payment processing, analytics, and a self-service member portal — all from one place.
Each gym is a tenant. Users can own or manage multiple gyms, and members can belong to more than one gym. The platform includes both an admin dashboard (for staff) and a member portal (for gym-goers), plus a public marketing site and embeddable join pages for member registration.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 5 |
| UI | shadcn/ui (New York) + Radix UI + Tailwind CSS v4 |
| State | TanStack Query + TanStack Table + TanStack Form |
| Database | Drizzle ORM → PGlite (dev) / Neon PostgreSQL (prod) |
| Auth | Better Auth (email + password, session cookies) |
| Payments | Stripe Connect (subscriptions, one-time, refunds) |
| Resend + React Email | |
| Charts | Recharts |
| Icons | Lucide React |
| i18n | Custom middleware (English + Spanish) |
| Toasts | Sonner |
| Package Manager | pnpm |
- Node.js >= 20
- pnpm >= 9
# Clone the repo
git clone https://github.com/your-org/kudeak.git
cd kudeak
# Install dependencies
pnpm install
# Set up environment variables
cp .env.example .env.local
# Create the data directory and seed the dev database
mkdir -p data/pglite
pnpm db:seed
# Start the dev server
pnpm devOpen http://localhost:3000.
After seeding, you can log in with:
| Role | Password | |
|---|---|---|
| Admin (owner) | admin@mail.com |
password |
| Manager | sarah.kim@mail.com |
password |
| Staff | jake@mail.com |
password |
| Member (portal) | sarah.johnson@email.com |
password |
Admin login: localhost:3000/login Member portal: localhost:3000/portal/login
| Command | Description |
|---|---|
pnpm dev |
Start dev server (Turbopack) |
pnpm build |
Production build |
pnpm start |
Run production server |
pnpm lint |
Run ESLint |
pnpm db:seed |
Seed PGlite with sample data (2 gyms, 35 members, classes, payments) |
pnpm db:generate |
Generate Drizzle migrations |
pnpm db:migrate |
Run migrations |
pnpm db:push |
Push schema to database |
pnpm db:studio |
Open Drizzle Studio GUI |
pnpm email:dev |
Preview email templates on port 3001 |
The app is split into four route groups, each with its own layout:
src/app/
(marketing)/ Marketing site + admin login
(dashboard)/ Admin dashboard (protected)
(portal)/ Member portal (protected)
(portal-auth)/ Portal login (public)
join/[gymSlug]/ Embeddable member registration (public)
api/ 40+ REST API endpoints
Every domain table has a gymId foreign key. The active gym is resolved server-side from a cookie (kudeak-active-gym-id), and client-side via GymProvider context.
User ──owns/manages──> Gym (via gym_members, role-based)
Gym ──has──> Members, Classes, Schedules, Bookings, Payments
Client Component
└─ TanStack Query (useMembers, useClasses, etc.)
└─ fetch("/api/members")
└─ API Route (auth check → gym scope → Drizzle query)
└─ PGlite (dev) or Neon PostgreSQL (prod)
src/lib/db/schema/
auth.ts users, sessions, accounts, verifications
gyms.ts gyms, gym_members
members.ts members, membership_plans
classes.ts classes, class_schedules
bookings.ts bookings
payments.ts payments
member-actions.ts member_actions (onboarding tasks)
member-signatures.ts member_signatures (waivers)
email-logs.ts email_logs
webhook-events.ts webhook_events
- API Routes + TanStack Query — no server actions. Every mutation goes through a REST endpoint.
- Custom hooks per domain —
use-members.ts,use-classes.ts, etc. Each scopes cache keys bygymId. - Cookie-based gym switching — both dashboard (
kudeak-active-gym-id) and portal (kudeak-portal-gym-id). - Middleware — protects dashboard/portal routes, detects locale, handles iframe CSP for join pages.
kudeak/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (marketing)/ # Landing page, admin login
│ │ ├── (dashboard)/ # Dashboard, members, schedule, payments, analytics, settings
│ │ ├── (portal)/ # Member portal (classes, actions, documents)
│ │ ├── (portal-auth)/ # Portal login
│ │ ├── join/[gymSlug]/ # Public member registration (embeddable)
│ │ └── api/ # REST API routes
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── layout/ # Sidebar, header, gym-switcher, mobile-nav
│ │ ├── dashboard/ # Metric cards, daily overview, activity feed
│ │ ├── members/ # Table, filters, forms, wizard
│ │ ├── schedule/ # Calendar grid, class cards, detail panel
│ │ ├── payments/ # Revenue chart, transactions, payouts
│ │ ├── analytics/ # Growth, forecasts, top classes, at-risk
│ │ ├── settings/ # Account, billing, branding, team, waiver tabs
│ │ ├── portal/ # Portal header, nav, waiver modal
│ │ └── docs/ # In-app documentation components
│ ├── lib/
│ │ ├── db/ # Drizzle connection + schema definitions
│ │ ├── stripe/ # Stripe helpers (resolve-gym, webhook handlers)
│ │ ├── auth.ts # Better Auth server config
│ │ ├── auth-client.ts # Client-side auth
│ │ ├── auth-utils.ts # getAuthSession()
│ │ ├── gym-utils.ts # getActiveGym()
│ │ └── portal-utils.ts # getPortalMember()
│ ├── hooks/ # TanStack Query hooks (per domain)
│ ├── providers/ # React context (query, gym, portal, locale)
│ ├── i18n/ # Internationalization (en, es)
│ ├── emails/ # React Email templates
│ ├── content/docs/ # MDX documentation content
│ └── middleware.ts # Auth guard, locale detection, iframe CSP
├── scripts/
│ └── seed.ts # Database seeding (2 gyms, full sample data)
├── docs/ # Architecture & implementation docs
├── design/ # UI design references (PNG + HTML)
└── data/pglite/ # Local PGlite database (gitignored)
- Overview — active members, monthly revenue, today's attendance, upcoming classes
- Member Management — searchable directory, filters by status/plan, multi-step add wizard, profile detail view
- Scheduling — weekly calendar grid, create/edit classes, manage schedules, view bookings per class
- Payments — revenue chart, transactions table, Stripe payout tracking, refund processing
- Analytics — member growth trends, revenue forecasts, top-performing classes, at-risk member alerts
- Settings — gym profile, account, team management (roles: owner/admin/manager/staff), branding (accent color + logo), waiver editor, notifications, billing
- Documentation — in-app searchable help center (MDX-powered)
- Dashboard — personalized greeting, next class, quick actions
- Class Browsing — view schedule, book/cancel classes
- Actions — pending onboarding tasks (sign waiver, complete profile)
- Documents — view signed waivers and documents
- Multi-gym — members at 2+ gyms get a gym switcher in the header
- Admins set an accent color and logo in Settings > Branding
- Applied to the public join page, portal header, and buttons
- Live preview in settings before saving
- Public URL:
/join/[gym-slug]— gym-branded, dark-themed registration - Can be embedded in external gym websites via
<iframe> - Middleware sets
frame-ancestors *CSP for/joinroutes
- English (default) and Spanish
- Auto-detected from cookie > IP country > Accept-Language header
- Locale switcher in the dashboard
- Stripe Connect — each gym connects their own Stripe account
- Subscriptions, one-time payments, manual charges
- Webhook handling for payment events
- Customer portal for self-service billing
- Plan syncing between Kudeak and Stripe
# Database — PGlite for dev (zero setup), Neon/PostgreSQL for prod
DATABASE_URL=pglite://./data/pglite
# Better Auth
BETTER_AUTH_SECRET=your-secret-here-at-least-32-characters
BETTER_AUTH_URL=http://localhost:3000
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Resend (email)
RESEND_API_KEY=re_your_api_key_hereFor production, set DATABASE_URL to your PostgreSQL connection string:
DATABASE_URL=postgresql://user:password@host:5432/dbname| Issue | Fix |
|---|---|
| PGlite WASM error on login | rm -rf data/pglite && mkdir -p data/pglite && pnpm db:seed |
tsx / esbuild breaks |
Delete node_modules/.pnpm/tsx@* and run pnpm install |
| Middleware deprecation warning | Harmless — Next.js 16 shows this but middleware still works |
| Build-time PGlite RuntimeError | Harmless — static generation can't load WASM, API routes work at runtime |
| Token | Value |
|---|---|
| Primary | #257bf4 (blue) |
| Marketing Accent | #fbbf24 (amber) |
| Font | Inter |
| Icons | Lucide React |
| Sidebar | 264px, white background |
| UI Style | shadcn/ui New York |
Private — not open source.