ShopySeed

Architecture Overview

System design, data flow, multi-tenancy implementation, and architectural decisions behind ShopySeed.

ShopySeed is built as a modern, scalable SaaS application using a monorepo architecture with TypeScript throughout the stack. This document explains the system design, data flow, and architectural decisions.

System Overview

┌─────────────────────────────────────────────────────────────────┐
│                          ShopySeed Architecture                    │
├─────────────────────────────────────────────────────────────────┤
│  Frontend (Next.js)          │  Backend (NestJS)                │
│  ┌─────────────────────┐    │  ┌─────────────────────┐         │
│  │ Next.js 14          │────┼─▶│ NestJS API          │         │
│  │ App Router          │    │  │ TypeScript Strict   │         │
│  │ Tailwind CSS        │    │  │ Prisma ORM          │         │
│  │ Shadcn/ui           │    │  │ JWT Authentication  │         │
│  └─────────────────────┘    │  └─────────────────────┘         │
│           │                 │            │                     │
│           │                 │            ▼                     │
│           │                 │  ┌─────────────────────┐         │
│           │                 │  │ PostgreSQL Database │         │
│           │                 │  │ Schema-based        │         │
│           │                 │  │ Multi-tenancy       │         │
│           │                 │  └─────────────────────┘         │
│           │                 │            │                     │
│           │                 │            ▼                     │
│           │                 │  ┌─────────────────────┐         │
│           │                 │  │ Redis Cache         │         │
│           │                 │  │ Session Storage     │         │
│           │                 │  └─────────────────────┘         │
│           │                 │                                  │
│           │                 │  ┌─────────────────────┐         │
│           └─────────────────┼─▶│ Stripe Integration  │         │
│                             │  │ Webhooks & Portal   │         │
│                             │  └─────────────────────┘         │
├─────────────────────────────────────────────────────────────────┤
│  Shared Packages                                                │
│  ┌─────────────────────┐  ┌─────────────────────┐             │
│  │ @shopyseed/shared     │  │ Turborepo           │             │
│  │ TypeScript Types    │  │ Build System        │             │
│  └─────────────────────┘  └─────────────────────┘             │
└─────────────────────────────────────────────────────────────────┘

Monorepo Structure

ShopySeed uses Turborepo to manage a TypeScript monorepo with optimized builds and shared dependencies.

Package Architecture

shopyseed/
├── apps/
│   ├── api/                    # Backend API (NestJS)
│   └── web/                    # Frontend Web App (Next.js)
├── packages/
│   └── shared/                 # Shared TypeScript Types
├── docker/                     # Docker configurations
├── docs/                       # Documentation
└── turbo.json                 # Turborepo configuration

Dependency Graph

┌─────────────┐    ┌─────────────┐
│     web     │    │     api     │
│  (Next.js)  │    │  (NestJS)   │
└─────┬───────┘    └─────┬───────┘
      │                  │
      └─────┬────────────┘

    ┌─────────────┐
    │   shared    │
    │   (Types)   │
    └─────────────┘

Benefits:

  • Type Safety: Shared types ensure API and frontend stay in sync
  • Fast Builds: Turborepo's caching optimizes build times
  • Atomic Updates: Changes to shared types update both apps
  • Code Reuse: Common utilities and types shared across packages

Backend Architecture (NestJS)

Module Structure

src/
├── app.module.ts               # Root application module
├── main.ts                     # Application bootstrap
├── common/                     # Shared utilities and guards
│   ├── decorators/             # Custom decorators
│   ├── guards/                 # Authentication & authorization guards
│   └── logger/                 # Structured logging
├── auth/                       # Authentication & authorization
│   ├── auth.controller.ts
│   ├── auth.service.ts
│   ├── auth.module.ts
│   ├── strategies/             # Passport strategies
│   └── dto/                    # Data transfer objects
├── organizations/              # Multi-tenant organizations
├── billing/                    # Stripe integration
├── health/                     # Health check endpoints
└── prisma/                     # Database module

Request Lifecycle

1. Request → Middleware → Guards → Controller → Service → Response
     │            │         │          │          │
     ▼            ▼         ▼          ▼          ▼
   CORS       JWT Auth   RBAC      Business    Database
 Rate Limit   Validation Checks     Logic       Access
  Logging      Tenant             Validation
             Resolution

Detailed Flow

  1. Middleware Layer

    • CORS handling for frontend access
    • Rate limiting to prevent abuse
    • Request logging for monitoring
    • Body parsing and validation
  2. Authentication Guard (JwtAuthGuard)

    • Validates JWT access token
    • Extracts user information
    • Handles refresh token rotation
  3. Organization Guard (OrganizationGuard)

    • Resolves tenant context
    • Sets PostgreSQL search_path
    • Ensures data isolation
  4. Authorization Guard (RolesGuard)

    • Checks user permissions (Owner/Admin/Member/Viewer)
    • Route-level access control
    • Hierarchical permission checking
  5. Controller Layer

    • HTTP request handling
    • Input validation with DTOs
    • Swagger documentation
  6. Service Layer

    • Business logic implementation
    • Database operations via Prisma
    • External API integrations

Multi-Tenancy Implementation

ShopySeed uses PostgreSQL schema-based isolation for multi-tenancy:

Tenant Isolation Flow

1. Request with Organization ID


2. OrganizationGuard validates access


3. Set search_path = 'tenant_abc123'


4. All queries run in tenant schema


5. Complete data isolation per tenant

Database Schema Structure

-- Shared schema (public)
public.users                    -- Global user accounts
public.organizations           -- Organization metadata
public.organization_members    -- User-organization relationships
public.subscriptions          -- Billing information

-- Per-tenant schemas (tenant_*)
tenant_abc123.projects        -- Tenant-specific data
tenant_abc123.tasks          -- Completely isolated
tenant_abc123.documents      -- No cross-tenant access

Benefits:

  • Complete Data Isolation: No risk of data leaks between tenants
  • Performance: Each tenant has optimized indexes
  • Scalability: Can move schemas to separate databases
  • Compliance: Meets strict data isolation requirements

Authentication Flow

JWT + Refresh Token Pattern

┌─────────────────┐                 ┌─────────────────┐
│   Frontend      │                 │   Backend       │
└─────────────────┘                 └─────────────────┘
         │                                   │
         │ 1. Login (email/password)         │
         ├─────────────────────────────────▶│
         │                                   │
         │ 2. Access Token + Refresh Token   │
         │◀─────────────────────────────────┤
         │                                   │
         │ 3. API Request + Access Token     │
         ├─────────────────────────────────▶│
         │                                   │
         │ 4. API Response                   │
         │◀─────────────────────────────────┤
         │                                   │
         │ 5. Token Expires (401)            │
         │◀─────────────────────────────────┤
         │                                   │
         │ 6. Refresh Request                │
         ├─────────────────────────────────▶│
         │                                   │
         │ 7. New Access Token               │
         │◀─────────────────────────────────┤

Token Strategy

  • Access Tokens: Short-lived (15 minutes), contains user + organization context
  • Refresh Tokens: Long-lived (7 days), single-use, stored in database
  • Rotation: Each refresh generates new refresh token, revokes old one
  • Security: Compromised tokens auto-expire, minimal attack window

Token Payload Structure

interface AccessTokenPayload {
  sub: string;           // User ID
  email: string;         // User email
  organizationId?: string; // Current organization context
  role?: MemberRole;     // Role in current organization
  iat: number;           // Issued at
  exp: number;           // Expires at
}

Frontend Architecture (Next.js)

App Router Structure

src/app/
├── layout.tsx                  # Root layout with providers
├── page.tsx                   # Landing page
├── globals.css                # Global styles
├── auth/                      # Authentication pages
│   ├── login/page.tsx
│   ├── register/page.tsx
│   └── email-verified/page.tsx
├── dashboard/                 # Protected dashboard area
│   ├── layout.tsx            # Dashboard layout with navigation
│   ├── page.tsx              # Dashboard home
│   ├── settings/             # User settings
│   ├── organization/         # Organization management
│   └── billing/              # Billing and subscriptions
└── api/                      # API routes (if needed)

State Management

ShopySeed uses React's built-in state management with strategic patterns:

Context Providers

// Authentication Context
const AuthContext = createContext<{
  user: User | null;
  organization: Organization | null;
  login: (credentials) => Promise<void>;
  logout: () => void;
  switchOrganization: (orgId: string) => void;
}>()

// Theme Context (for dark/light mode)
const ThemeContext = createContext<{
  theme: 'light' | 'dark';
  setTheme: (theme: Theme) => void;
}>()

Data Fetching Strategy

  • Server Components: For initial page loads and SEO-critical content
  • Client Components: For interactive components and real-time data
  • SWR/React Query: For client-side data fetching with caching
  • API Routes: For form submissions and client-server communication

Component Architecture

components/
├── ui/                        # Base UI components (Shadcn/ui)
│   ├── button.tsx
│   ├── input.tsx
│   └── dialog.tsx
├── forms/                     # Form components
│   ├── auth-forms.tsx
│   ├── organization-forms.tsx
│   └── billing-forms.tsx
├── layout/                    # Layout components
│   ├── navigation.tsx
│   ├── sidebar.tsx
│   └── header.tsx
└── features/                  # Feature-specific components
    ├── auth/
    ├── dashboard/
    ├── organizations/
    └── billing/

Billing Integration (Stripe)

Stripe Webhook Flow

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│     Stripe      │    │   ShopySeed API   │    │   Database      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │ 1. Subscription Event  │                       │
         ├─────────────────────▶ │                       │
         │                       │                       │
         │                       │ 2. Verify Signature   │
         │                       ├──────────────────────▶│
         │                       │                       │
         │                       │ 3. Update Subscription│
         │                       ├──────────────────────▶│
         │                       │                       │
         │ 4. Acknowledge (200)   │                       │
         │◀─────────────────────── │                       │

Supported Webhook Events

  • customer.subscription.created - New subscription
  • customer.subscription.updated - Plan changes, renewals
  • customer.subscription.deleted - Cancellations
  • invoice.payment_succeeded - Successful payments
  • invoice.payment_failed - Failed payments

Subscription Management

interface Subscription {
  id: string;
  organizationId: string;
  stripeCustomerId: string;
  stripeSubscriptionId: string;
  plan: string;                    // 'free' | 'starter' | 'pro' | 'enterprise'
  status: SubscriptionStatus;      // 'active' | 'past_due' | 'canceled' | 'trialing'
  currentPeriodStart: DateTime;
  currentPeriodEnd: DateTime;
  cancelAtPeriodEnd: boolean;
}

Plan Enforcement

  • Usage Limits: Enforced at API level based on subscription plan
  • Feature Gates: Frontend components conditionally render based on plan
  • Billing Guards: NestJS guards prevent API access for expired subscriptions

Data Flow Patterns

API Request Flow

Frontend              Backend               Database
   │                     │                     │
   │ 1. HTTP Request     │                     │
   ├────────────────────▶│                     │
   │                     │                     │
   │                     │ 2. Auth Validation  │
   │                     ├────────────────────▶│
   │                     │                     │
   │                     │ 3. Business Logic   │
   │                     │                     │
   │                     │ 4. Database Query   │
   │                     ├────────────────────▶│
   │                     │                     │
   │                     │ 5. Query Results    │
   │                     │◀────────────────────┤
   │                     │                     │
   │ 6. HTTP Response    │                     │
   │◀────────────────────┤                     │

Error Handling Strategy

Backend Error Handling

// Global Exception Filter
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    
    if (exception instanceof HttpException) {
      // Handle known HTTP exceptions
      return response.status(exception.getStatus()).json({
        statusCode: exception.getStatus(),
        message: exception.message,
        timestamp: new Date().toISOString(),
      });
    }
    
    // Handle unknown errors
    logger.error('Unexpected error:', exception);
    return response.status(500).json({
      statusCode: 500,
      message: 'Internal server error',
    });
  }
}

Frontend Error Boundaries

// Error Boundary for graceful error handling
class ErrorBoundary extends Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

Security Architecture

Defense in Depth

┌─────────────────────────────────────────────────────────────┐
│                    Security Layers                          │
├─────────────────────────────────────────────────────────────┤
│ 1. Network Level                                            │
│    • HTTPS/TLS encryption                                   │
│    • CORS policy enforcement                                │
│    • Rate limiting per IP                                   │
├─────────────────────────────────────────────────────────────┤
│ 2. Application Level                                        │
│    • JWT token validation                                   │
│    • Input validation & sanitization                       │
│    • SQL injection prevention (Prisma)                     │
├─────────────────────────────────────────────────────────────┤
│ 3. Authorization Level                                      │
│    • Role-based access control                             │
│    • Resource-level permissions                            │
│    • Multi-tenant data isolation                           │
├─────────────────────────────────────────────────────────────┤
│ 4. Data Level                                              │
│    • Database connection encryption                        │
│    • Schema-based tenant isolation                         │
│    • Sensitive data hashing                                │
└─────────────────────────────────────────────────────────────┘

Security Headers

// Helmet configuration for security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      scriptSrc: ["'self'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
}));

Performance Considerations

Database Optimization

  • Indexing Strategy: Optimized indexes for multi-tenant queries
  • Connection Pooling: Prisma connection pool optimization
  • Query Optimization: N+1 query prevention with Prisma includes

Caching Strategy

// Redis caching for frequently accessed data
@Injectable()
export class CacheService {
  async get<T>(key: string): Promise<T | null> {
    const cached = await this.redis.get(key);
    return cached ? JSON.parse(cached) : null;
  }
  
  async set(key: string, value: any, ttl: number = 3600): Promise<void> {
    await this.redis.setex(key, ttl, JSON.stringify(value));
  }
}

Frontend Performance

  • Code Splitting: Automatic route-based code splitting
  • Image Optimization: Next.js Image component with optimization
  • Bundle Analysis: Webpack bundle analyzer integration

Monitoring and Observability

Structured Logging

// Structured logging service
@Injectable()
export class LoggerService {
  info(message: string, meta?: Record<string, any>) {
    console.log(JSON.stringify({
      level: 'info',
      message,
      timestamp: new Date().toISOString(),
      ...meta,
    }));
  }
}

Health Checks

  • /health - Basic application health
  • /health/ready - Database and Redis connectivity
  • Kubernetes-compatible health endpoints

Deployment Architecture

Docker Multi-Stage Builds

# Multi-stage build for production optimization
FROM node:20-alpine AS base
# ... base configuration

FROM base AS dependencies  
# Install dependencies

FROM base AS build
# Build applications  

FROM base AS runtime
# Production runtime

Environment Separation

  • Development: Docker Compose with hot reloading
  • Staging: Kubernetes with production-like configuration
  • Production: Kubernetes with full monitoring and backup

Scalability Patterns

Horizontal Scaling

  • Stateless API: No server-side session state
  • Database Read Replicas: Read queries can be distributed
  • CDN Integration: Static assets served from CDN
  • Microservice Ready: Modular architecture allows service extraction

Vertical Scaling

  • Database Optimization: Query optimization and indexing
  • Connection Pooling: Efficient database connection management
  • Memory Management: Optimized for low memory footprint

This architecture provides a solid foundation for building scalable, secure SaaS applications. Each component is designed with production requirements in mind, including security, performance, and maintainability.

On this page