HeartCo SaaS Architecture
Table of Contents
- Overview
- System Architecture Diagram
- Authentication & Authorization Flow
- Stripe Billing Flow
- Mobile (Expo) Integration
- Directory Structure
- tRPC Procedure Hierarchy
- Data Scoping & Multi-Tenancy
- Key Design Decisions
- Security Essentials
- Development Commands
- Monitoring & Observability
- Deployment
- Next Steps
Overview
HeartCo is a multi-tenant SaaS platform built on Next.js 15 App Router with tRPC for type-safe RPC, Prisma 7 for ORM, PostgreSQL (Supabase) for data, NextAuth v5 for authentication, and Mistral AI for intelligent features. The platform includes web (React), mobile (Expo), and specialized verticals (Payroll, Accounting) with Stripe billing, Redis-cached RBAC, and comprehensive audit trails.
System Architecture Diagram
┌─────────────────┐ ┌────────────────────────┐ ┌──────────────────┐
│ Browser (SPA) │────▶│ Next.js 15 │────▶│ PostgreSQL │
│ React + tRPC │◀────│ App Router + Middleware│◀────│ (Supabase) │
└─────────────────┘ │ │ └──────────────────┘
│ ┌──────────────────┐ │ ┌──────────────────┐
┌─────────────────┐ │ │ tRPC Procedures │ │────▶│ Redis / Upstash │
│ Expo Mobile │────▶│ │ (staffProcedure, │ │◀────│ (Cache + Queues)│
│ App │◀────│ │ requirePermit) │ │ └──────────────────┘
└─────────────────┘ │ └──────────────────┘ │ ┌──────────────────┐
│ │────▶│ Stripe API │
┌─────────────────┐ │ ┌──────────────────┐ │ └──────────────────┘
│ External APIs │────▶│ │ Prisma 7 Client │ │ ┌──────────────────┐
│ (Stripe, │◀────│ │ (org-scoped via │ │────▶│ Resend / Email │
│ Mistral, etc) │ │ │ $extends) │ │ └──────────────────┘
└─────────────────┘ │ └──────────────────┘ │ ┌──────────────────┐
│ │────▶│ Mistral AI │
│ ┌──────────────────┐ │ └──────────────────┘
│ │ Webhooks Handler │ │ ┌──────────────────┐
│ │ (Stripe, Bridge) │ │────▶│ Pusher / Queues │
│ └──────────────────┘ │ └──────────────────┘
└────────────────────────┘
Authentication & Authorization Flow
1. User Login
└─→ NextAuth v5 Provider (CredentialsProvider or OAuth)
└─→ Verify credentials / OAuth token
└─→ Create JWT session (signed, httpOnly cookie)
2. Middleware (src/middleware.ts)
└─→ Check session validity + refresh if needed
└─→ Enforce route protection (public/private/admin)
└─→ Inject session into context
3. tRPC Procedure Call
└─→ Check procedure guard (protectedProcedure, staffProcedure, etc.)
└─→ Load user permissions from Redis (cached)
└─→ DENY-by-default: check requirePermission() guards
└─→ Execute procedure with organizationId context
4. Data Access (Prisma)
└─→ Prisma $extends auto-filters by organizationId
└─→ Query results scoped to caller's organization only
Stripe Billing Flow
1. User selects plan → Checkout Page
└─→ Redirect to Stripe Checkout Session
└─→ User completes payment
2. Stripe Webhook → /api/stripe/webhook
└─→ Verify HMAC signature (timingSafeEqual)
└─→ Handle events:
• checkout.session.completed → create Subscription record
• customer.subscription.updated → update plan + features
• customer.subscription.deleted → downgrade to FREE
• invoice.payment_failed → trigger dunning flow (email)
3. Database Update
└─→ Update Subscription model (plan, features, periodEnd)
└─→ Reset monthly usage counters if needed
└─→ Send confirmation email (Resend)
4. Customer Portal
└─→ Link in dashboard → manage.stripe.com (session creation)
└─→ Update payment method, cancel subscription
Mobile (Expo) Integration
1. Mobile Auth
└─→ JWT endpoint → /api/auth/mobile-token
└─→ Exchange mobile credentials → return JWT
└─→ Store in SecureStore (React Native)
2. tRPC from Mobile
└─→ Same backend API (no separate mobile endpoint)
└─→ React Query / TRPC client for offline queue
└─→ Queue mutations locally (SQLite)
3. Sync on Reconnect
└─→ Check connection status
└─→ Flush outbox queue → tRPC mutations
└─→ Sync remote data (pull changes)
└─→ Push notifications (Expo Notification Service)
4. Deep Linking
└─→ Expo.Linking → route to in-app screen with parameters
Directory Structure
/src
/app # Next.js App Router (pages + layouts)
/api # Route handlers (webhooks, auth, crons)
/dashboard # Protected dashboard routes
/(marketing) # Public landing & marketing pages
/auth # Auth flows (login, signup, reset)
/server
/api
/routers # 76 tRPC routers
/crm.ts # Contacts, deals, activities, tasks
/facturation.ts # Invoicing, payments, dunning
/comptabilite.ts # Accounting, journal entries, GL
/appointments.ts # Scheduling, calendars, resources
/automation.ts # Workflows, triggers, email campaigns
/chatbot.ts # AI chatbot, conversation management
/analytics.ts # Custom dashboards, KPIs
/bank.ts # Bank integration (Bridge API), reconciliation
[+25 more]
/root.ts # Router registry (combines all)
/trpc.ts # Procedure guards + middleware
/abtest.ts # A/B testing utilities
/lib
/permissions
/matrix.ts # RBAC matrix (source of truth)
/permissions.ts # Helper functions
/prisma-org-scope.ts # Prisma $extends for auto-scoping
/auth/ # NextAuth config, callbacks
/stripe/ # Stripe utilities
/ai/ # Mistral AI integration
/freemium/ # Usage tracking & quotas
/cache.ts # Redis wrapper
/validation.ts # Zod schemas
/utils.ts # Helpers (cn(), etc.)
/components
/ui # shadcn/Radix primitives (Button, Card, etc.)
/layout # Sidebar, navbar, main layout
/auth # Login, signup, password forms
/crm # CRM widgets
/dashboard # Dashboard charts, tables
/landing # Landing page sections
/modules
/bridge/ # Bank API integration
/facturx/ # Factur-X e-invoice (certified)
/iopole/ # Compliance gateway
/payment/ # Payment processing
/hooks # Custom React hooks (useOrgDb, usePermissions, etc.)
/contexts # React Context providers (theme, auth, notifications)
/stores # Client state (Zustand, if used)
/types # TypeScript type definitions
/constants # Env values, feature flags
/test # Test utilities, mocks
/__tests__ # Integration/security tests
/middleware.ts # Next.js middleware (auth, ratelimit)
/env.js # Environment validation (@t3-oss/env-nextjs)
/apps
/mobile # Expo React Native app
/app-terrain # Field worker app (specialized)
/app-temps # Temp staffing app (specialized)
/packages
/shared-types # Monorepo shared TS types (if used)
/prisma
/schema.prisma # Prisma schema (models + relations)
/migrations/ # Auto-generated migration files
/generated
/prisma/ # Prisma client (generated, not in node_modules)
/e2e
/tests/ # Playwright E2E tests
/.auth/user.json # Saved auth state (gitignored)
tRPC Procedure Hierarchy
| Level | Name | Requirement | Use Case |
|---|---|---|---|
| 0 | publicProcedure | None | Public routes, webhooks (no auth) |
| 1 | protectedProcedure | Valid session (any role) | Client portal, personal data |
| 2 | staffProcedure | Valid session + not CLIENT role | Internal tools, staff features |
| 3 | managerProcedure | ADMIN, DIRECTION, MANAGER, HR | Payroll, reports, team management |
| 4 | adminProcedure | ADMIN role only | System settings, user management |
| 5 | requirePermission("res:act") | staffProcedure + specific RBAC permission | Granular access (e.g., "invoice:export") |
Pattern: Always start with the most restrictive guard needed. Stack guards with requirePermission() for fine-grained control.
Data Scoping & Multi-Tenancy
// Prisma $extends auto-filters all reads by organizationId
const ctx.orgDb = ctx.db.$extends(
orgScope({ organizationId: ctx.session.user.organizationId })
);
// Usage:
const contacts = await ctx.orgDb.contact.findMany(); // auto-filtered by org
// For writes, add organizationId manually:
await ctx.db.contact.create({
data: {
...input,
organizationId: ctx.session.user.organizationId, // REQUIRED
},
});
// For deletes, ALWAYS include organizationId in where:
await ctx.db.contact.delete({
where: {
id: input.id,
organizationId: ctx.session.user.organizationId, // REQUIRED for safety
},
});Rule: Every data model with business data must have organizationId String + @@index([organizationId]).
Key Design Decisions
-
Multi-Tenant Isolation via Prisma $extends
- Single database, one table per entity
- Auto-filtering on reads (
ctx.orgDb) - Manual checks on writes (organizationId in
whereclause) - IDOR prevention: respond with NOT_FOUND (never FORBIDDEN)
-
RBAC with Redis-Cached Permissions
- Permissions matrix in
src/lib/permissions/matrix.ts(source of truth) - Loaded from Redis on every request (TTL: 5min)
- DENY-by-default: all features blocked until explicitly granted
- Stack guards:
requirePermission("invoice:create")on top ofstaffProcedure
- Permissions matrix in
-
Type Safety & Strictness
- TypeScript strict mode enabled (
noUncheckedIndexedAccess: true) - All type imports:
import type { Foo }orimport { type Foo } - No
anywithout justification (useunknown+ type guards) - Zod for runtime validation of tRPC inputs
- TypeScript strict mode enabled (
-
Prisma Client in generated/prisma/ (not node_modules)
- Avoids bloated node_modules
- Faster CI/CD
- Explicit output directory in schema.prisma
-
Tailwind v4 + shadcn/ui (Radix Primitives)
- Semantic CSS variables (dark mode built-in)
- Never hardcode colors
- Class merging via
cn()from utils.ts
-
Freemium Model with Usage Tracking
- Limits per plan in
src/lib/freemium/freemium-limits.ts - Check before action:
await checkFreemiumLimit(ctx, "feature") - Increment after success (not before)
- Monthly reset via cron job
- Limits per plan in
Security Essentials
- Webhooks: Verify HMAC with
crypto.timingSafeEqual(never===) - Error Messages: Hide IDOR leaks with NOT_FOUND (not FORBIDDEN)
- Session: httpOnly cookies, JWT signed, short expiry with refresh token
- Middleware: Rate limit + session check on every request
- Permissions: DENY-by-default, no wildcard grants
- Audit Log: Every create/update/delete logged with user + timestamp + changes
Development Commands
pnpm dev # Next.js dev server (Turbopack)
pnpm build # Prod build
pnpm start # Start prod server
# Code quality
pnpm lint # ESLint check
pnpm lint:fix # ESLint auto-fix
pnpm format:write # Prettier format
pnpm typecheck # tsc --noEmit
# Testing
pnpm test # Vitest (unit tests)
pnpm test:security # Permission/RBAC tests
pnpm test:watch # Watch mode
# Database
npx prisma generate # Regenerate Prisma client (→ generated/prisma/)
npx prisma migrate dev --name <name> # Create + run migration
npx prisma studio # Visual DB browserMonitoring & Observability
- Error Tracking: Sentry integration (env: SENTRY_DSN)
- Analytics: Segment / Mixpanel custom events
- Webhooks Delivery: Svix or Hookdeck (retry + logging)
- Performance: Vercel Analytics (Web Vitals)
- Logs: JSON structured logs (Winston / Pino)
Deployment
- Primary: Vercel (Next.js native)
- Database: Supabase PostgreSQL (auto-backups, point-in-time restore)
- Cache: Upstash Redis (serverless)
- Email: Resend (transactional)
- Storage: Supabase Storage or S3 (documents, invoices)
- Webhooks: Stripe, Bridge, custom integrations
All secrets stored in .env.local (never committed). Vercel Environment variables for production.
Next Steps
- Read
/claude/rules/for domain-specific conventions (Freemium, Security, Testing, Git) - Run
pnpm install && pnpm devto start local development - Review
src/server/api/root.tsto understand router structure - Check
src/lib/permissions/matrix.tsfor your role's capabilities - Use
pnpm test:securityto verify RBAC enforcement