A production-grade, multitenant B2B SaaS platform with 4-tier RBAC, org-scoped data isolation, and a white-label client portal β engineered for modern digital agencies.
ClientFlow is a comprehensive demonstration of my ability to architect, build, and deploy complex full-stack applications. It goes far beyond a typical CRUD app by addressing hard, real-world engineering challenges:
- Complex Data Architecture: Engineered a rock-solid multitenant PostgreSQL database schema where user data is strictly cordoned off by their Organization ID.
- Enterprise Security: Implemented an implicit "Iron Curtain" data-access proxy to prevent cross-tenant data leaksβbacked by robust integration tests.
- Modern React Paradigms: Extensively leverages Next.js 15 React Server Components (RSC) to minimize client-side javascript while delivering blazing fast edge performance.
- Production Resilience: Solved database cold-start timeouts and connection scaling limits characteristic of serverless environments by implementing robust singleton connection pooling and intuitive React Error Boundaries.
- Polished UX: Designed a premium, agency-quality interface demonstrating advanced knowledge of CSS variables, dynamic theming (dark/light mode), and subtle micro-interactions.
Agency Dashboard featuring real-time activity feeds, live task metrics, and role-based access controls.
To experience the complete data isolation first-hand, log into these two tenants in separate browser windows to verify the "Iron Curtain" β there is absolutely no cross-tenant data leakage.
| Tenant | Role | Password | |
|---|---|---|---|
| Pixel Agency (Pro) | Admin | alice@pixel.co |
password123 |
| Manager | bob@pixel.co |
password123 |
|
| Member | carol@pixel.co |
password123 |
|
| Client | dave@acme.com |
password123 |
|
| Nova Studio (Starter) | Admin | admin@nova.io |
password123 |
Leak Test: Log into Pixel Agency in one tab and Nova Studio in an incognito window. Copy a project URL from one and paste it into the other β you'll hit a
403 Forbiddenor404. That's the org-scoped isolation natively working.
| Area | What Was Built |
|---|---|
| Security & Validation | Engineered a centralized validation layer using Zod, ensuring type-safety across the entire JavaScript monolith. Every API route validates input through shared schemas before touching the database. |
| Data Isolation | Built an org-scoped query proxy (withOrgScope) that automatically injects orgId into every Prisma query β making it architecturally impossible to access another tenant's data, even by accident. |
| Infrastructure & Auditability | Implemented an immutable Audit Log system, providing a tamper-proof history of every organizational change (project CRUD, member invites, role changes, file uploads) for compliance and transparency. |
| White-Label Architecture | Developed a dynamic theming engine that injects tenant-specific CSS variables (primaryColor) via the root layout, allowing full brand customization for agency tenants without CSS bloat. |
| Rate Limiting | Integrated Upstash Redis sliding-window rate limiters on auth and invite endpoints to prevent brute-force attacks at the edge. |
| File Management | Built secure S3-backed file uploads using presigned URLs β the server never touches the file bytes, keeping the serverless footprint minimal. |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT (Browser) β
β React Server Components + Client Components (Next.js 15) β
β <Can permission="..."> RBAC gate β Optimistic UI updates β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β
ββββββββΌβββββββ
β Middleware β β Session validation
ββββββββ¬βββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β β β
ββββββββΌβββββββ βββββββΌβββββββ ββββββββΌβββββββ
β API Routes β β NextAuth β β Webhooks β
β /api/* β β JWT + DB β β Stripe β
ββββββββ¬βββββββ ββββββββββββββ βββββββββββββββ
β
ββββββββΌββββββββββββββββββββββββββββββββββββββββ
β SECURITY LAYER β
β assertPermission(session, "createProject") β
β withOrgScope(orgId).project.findMany(...) β
β validate(createProjectSchema, body) β
β checkRateLimit(authLimiter, ip) β
ββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
ββββββββΌβββββββ ββββββββββββ ββββββββββββ
β PostgreSQL β β AWS S3 β β Upstash β
β (Neon) β β Files β β Redis β
βββββββββββββββ ββββββββββββ ββββββββββββ
| Permission | Admin | Manager | Member | Client |
|---|---|---|---|---|
| Create projects | β | β | β | β |
| Delete projects | β | β | β | β |
| Create/update tasks | β | β | β | β |
| Upload files | β | β | β | β |
| Comment on projects | β | β | β | β |
| Invite members | β | β | β | β |
| Change member roles | β | β | β | β |
| View billing/plan | β | β | β | β |
| Update/delete org | β | β | β | β |
| View all projects | β | β | β | β |
| View assigned only | β | β | β | β |
Permissions are enforced twice: on the server via assertPermission() in every API route, and on the client via the <Can> component that conditionally renders UI elements.
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 15 (App Router) | Stable RSC support, file-based routing, middleware |
| Database | PostgreSQL (Neon) | Serverless Postgres with connection pooling |
| ORM | Prisma 6 | Type-safe queries, migrations, seeding |
| Auth | NextAuth.js v5 | JWT sessions, Credentials provider, PrismaAdapter |
| Styling | Tailwind CSS v4 + Lucide | Utility-first CSS, tree-shakeable icons |
| Rate Limiting | Upstash Redis | Edge-compatible sliding window limiters |
| Storage | AWS S3 | Presigned uploads β zero server bytes |
| Resend | Transactional invite emails with HTML templates | |
| Payments | Stripe | Checkout, Customer Portal, webhook sync |
| Validation | Zod | Runtime schema validation for all API inputs |
| Testing | Vitest | 8 multitenancy isolation tests |
| Deployment | Vercel | Instant deploys, edge middleware |
- Multi-tenant isolation β Every DB query scoped by orgId. Tenant A physically cannot access Tenant B's data.
- 4-tier RBAC β Admin, Manager, Member, Client with 15 granular permissions enforced server AND client side
- Real-time chat β Pusher-powered instant messaging per organization with typing indicators
- Member workload tracking β See what every team member is working on, their task completion rate, and availability status
- Activity log β Immutable audit trail of every action
- File uploads β Presigned Cloudflare R2 URLs, server never touches file bytes
- Invite system β Token-based email invites via Resend, 48h expiry, role pre-assignment
- Light/Dark mode β Full theme system with CSS variables, persisted via localStorage
- Graceful degradation β App works without Stripe/S3/Email configured β services fail silently with helpful UI messages
ClientFlow is intentionally built as a modular monolith. For a B2B SaaS serving agencies with <10K tenants, the overhead of microservices (service mesh, distributed tracing, eventual consistency) introduces complexity without proportional benefit. The monolith gives us:
- Transactional integrity β Deleting an org cascades through projects, tasks, files, comments, and invites in a single ACID transaction.
- Shared type safety β Zod schemas and Prisma types are shared across the entire codebase. No contract drift.
- Deployment simplicity β One
vercel --proddeploys everything. No orchestration layer needed.
The architecture is nonetheless extraction-ready: lib/ modules (auth.js, stripe.js, s3.js, email.js) are self-contained and could be extracted into serverless functions or microservices if scale demands it.
Every database query in the app is scoped to the authenticated user's orgId. This is enforced at three levels:
-
Schema Design β Every model (Project, Task, File, Comment, AuditLog) has an
orgIdforeign key. There is no data without an owner. -
Query Proxy β The
withOrgScope(orgId)utility wraps Prisma delegates in a Proxy that automatically injectsorgIdinto everywhereclause:// Instead of risking: prisma.project.findMany({}) // We enforce: withOrgScope(session.user.orgId).project.findMany({}) // The proxy ensures orgId is ALWAYS present.
-
API Layer β Every route handler extracts
orgIdfrom the JWT session and passes it down. There is no code path that can skip this.
This architecture was validated with 8 dedicated multitenancy tests (Vitest) that attempt cross-tenant reads, writes, and deletes β all of which correctly return 403 or 404.
A key challenge deploying to Vercel was Next.js's aggressive static analysis during builds. Libraries like Prisma and Stripe attempt to connect to external services when their modules are imported β which fails during the build phase where no database exists.
Solution: Lazy-loading Proxies. Both prisma and stripe clients are wrapped in ES6 Proxy objects that defer initialization until the first actual method call:
export const prisma = new Proxy({}, {
get(target, prop) {
if (!_prisma) _prisma = new PrismaClient();
return _prisma[prop];
}
});This ensures zero side-effects at import time β the clients only wake up when a user actually triggers a request.
app/
βββ (app)/dashboard/ # Authenticated dashboard pages
β βββ page.js # Main dashboard (Server Component)
β βββ members/ # Team management (RBAC-gated)
β βββ projects/[id]/ # Project detail: Tasks / Files / Comments tabs
β βββ settings/ # Org settings + billing + audit log + danger zone
βββ (auth)/ # Login & Signup flows
βββ api/ # 15 REST API routes
β βββ auth/[...nextauth]/ # NextAuth with rate-limited POST
β βββ billing/ # Stripe checkout & portal
β βββ invites/ # Token-based team invites + accept flow
β βββ members/ # Member CRUD + role changes
β βββ orgs/ # Org CRUD + audit export (CSV)
β βββ projects/ # Project CRUD + tasks/files/comments
β βββ webhooks/stripe/ # Stripe webhook β plan sync
βββ onboarding/ # Post-signup org creation flow
βββ page.js # Landing page
lib/
βββ auth.js # NextAuth v5 config (JWT + PrismaAdapter)
βββ permissions.js # RBAC permission map + assertPermission()
βββ orgScope.js # Org-scoped Prisma query proxy
βββ prisma.js # Lazy-init Prisma client singleton
βββ rateLimit.js # Upstash sliding window rate limiters
βββ stripe.js # Lazy-init Stripe client
βββ s3.js # S3 presigned URL helpers
βββ email.js # Resend HTML email templates
βββ audit.js # Immutable audit trail logger
βββ validations.js # Zod schemas for all API inputs
βββ env.js # Build-safe environment validation
βββ plans.js # Plan feature definitions
components/
βββ Can.jsx # Client-side RBAC gate component
__tests__/
βββ multitenancy.test.js # 8 tenant isolation tests
βββ helpers/ # Seed data & session mocking
- Node.js 18+
- PostgreSQL database (or use Neon free tier)
- npm or yarn
# 1. Clone and install
git clone https://github.com/1tsadityaraj/ClientFlow.git
cd ClientFlow
npm install
# 2. Configure environment
cp .env.example .env
# Open .env and fill in required values:
# - DATABASE_URL (from Neon or local PostgreSQL)
# - NEXTAUTH_SECRET (run: openssl rand -base64 32)
# - NEXTAUTH_URL=http://localhost:3000
# 3. Setup database
npx prisma migrate dev
npx prisma db seed
# 4. Start development server
npm run devInstant demo login (after seeding):
- Admin: alice@pixel.co / password123
- Client: dave@acme.com / password123
These services enhance the app but it works without them:
- Pusher β Real-time chat (falls back to polling)
- Resend β Invite emails (logs to console in dev)
- Cloudflare R2 β File uploads (button disabled without it)
- Upstash Redis β Rate limiting (skipped without it)
npm run test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage reportThe test suite validates multitenancy isolation β ensuring that a user from Org A cannot read, update, or delete resources belonging to Org B across all API endpoints (projects, tasks, files, members).
- PostgreSQL database (Neon)
- Environment variables on Vercel
- Prisma migrations deployed
- Demo data seeded (2 tenants, 6 users, 5 projects)
- Upstash Redis (rate limiting active)
- Cloudflare R2 (file storage active)
- Resend (invite emails active)
- Stripe (India invite-only β pending)
- Set Environment Variables: Add all variables from
scripts/vercel-env-list.shto your Vercel project settings. - Setup Database: From your local terminal (ensure
DATABASE_URLpoints to production), run:npm run db:prod:setup
- Verify Seed: Log in as an admin and visit
/api/admin/seed-checkto confirm the database is correctly populated. - Deploy: Push your changes to the
mainbranch:git add . git commit -m "feat: complete activity log and production setup" git push origin main
Contributions are welcome! Please:
- Fork the repo
- Create a feature branch: git checkout -b feat/your-feature
- Commit your changes: git commit -m 'feat: add your feature'
- Push to branch: git push origin feat/your-feature
- Open a Pull Request
MIT
Built with β€οΈ by Aditya Raj

