Skip to main content

Monorepo Overview

The JOIP Web Application follows a monorepo architecture with three main directories:
Joip-Web-App-2/
├── client/          # React frontend application
├── server/          # Express.js backend API
├── shared/          # Shared TypeScript types and schemas
├── docs/            # Documentation (Mintlify)
├── scripts/         # Build and utility scripts
├── public/          # Static assets
└── package.json     # Root package.json for entire monorepo
This is a single-package monorepo - all code shares the same node_modules and package.json. There are no separate packages or workspaces.

Client Directory Structure

The client/ directory contains the React frontend application built with Vite:
client/
├── src/
│   ├── components/      # Reusable React components
│   │   ├── ui/          # Radix UI primitives (Button, Dialog, etc.)
│   │   ├── sessions/    # Session-related components
│   │   ├── NavItems.tsx # Navigation menu items
│   │   └── ...
│   ├── pages/           # Route-level page components
│   │   ├── SessionsPage.tsx
│   │   ├── EditSessionPage.tsx
│   │   ├── SessionPlayerPage.tsx
│   │   ├── ManualSessionCreationPage.tsx
│   │   ├── ManualSessionEditPage.tsx
│   │   ├── SmartCaptionsPage.tsx
│   │   ├── BabecockStudioPage.tsx
│   │   ├── MediaVaultPage.tsx
│   │   ├── CommunityPage.tsx
│   │   ├── AdminPanelPage.tsx
│   │   └── ...
│   ├── gaslighter/      # Gaslighter feature module
│   │   ├── components/  # Gaslighter-specific components
│   │   ├── GaslighterPage.tsx
│   │   └── utils.ts
│   ├── scroller/        # Scroller feature module
│   │   ├── components/  # Scroller-specific components
│   │   ├── ScrollerPage.tsx
│   │   └── utils.ts
│   ├── hooks/           # Custom React hooks
│   │   ├── use-toast.ts
│   │   └── ...
│   ├── lib/             # Utility libraries
│   │   ├── AuthContext.tsx   # Authentication context
│   │   ├── utils.ts          # Utility functions
│   │   └── queryClient.ts    # React Query setup
│   ├── styles/          # Global styles
│   ├── assets/          # Images, icons, fonts
│   ├── App.tsx          # Main application component with routing
│   ├── main.tsx         # Application entry point
│   └── index.css        # Global CSS and Tailwind imports
├── index.html           # HTML entry point
└── vite.config.ts       # Vite configuration

Key Client Files

App.tsx - Application Routes

Defines all application routes using Wouter:
import { Route, Switch } from "wouter";
import { AuthWrapper } from "./lib/AuthWrapper";
import SessionsPage from "./pages/SessionsPage";
import EditSessionPage from "./pages/EditSessionPage";
import SessionPlayerPage from "./pages/SessionPlayerPage";
// ... more imports

function App() {
  return (
    <Switch>
      <Route path="/" component={LandingPage} />
      <Route path="/login" component={LoginPage} />
      
      {/* Protected routes with AuthWrapper */}
      <Route path="/sessions">
        <AuthWrapper><SessionsPage /></AuthWrapper>
      </Route>
      
      <Route path="/sessions/:id/play">
        <AuthWrapper><SessionPlayerPage /></AuthWrapper>
      </Route>
      
      {/* ... more routes */}
    </Switch>
  );
}

lib/AuthContext.tsx - Authentication State

Manages user authentication state across the application:
import { createContext, useContext, useState, useEffect } from "react";

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
}

export const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  // Fetch current user on mount
  useEffect(() => {
    fetch('/api/auth/user')
      .then(res => res.json())
      .then(data => setUser(data.user))
      .finally(() => setIsLoading(false));
  }, []);

  // ... login and logout implementations

  return (
    <AuthContext.Provider value={{ user, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

Feature Module Pattern

Some features like Gaslighter and Scroller are organized as self-contained modules:
gaslighter/
├── components/
│   ├── GaslighterGrid.tsx
│   ├── GaslighterSettings.tsx
│   └── MediaModal.tsx
├── GaslighterPage.tsx     # Main page component
├── utils.ts               # Feature-specific utilities
└── types.ts               # Feature-specific types
Feature modules contain all components and logic specific to that feature, but still use shared UI components from components/ui/ and utilities from lib/.

Server Directory Structure

The server/ directory contains the Express.js backend API:
server/
├── index.ts                 # Server entry point and Express setup
├── routes.ts                # API route definitions
├── db.ts                    # Database connection and pool config
├── storage.ts               # Database CRUD operations (IStorage)
├── vite.ts                  # Vite middleware for development
├── auth.ts                  # Authentication strategies
├── authUtils.ts             # Auth helper functions
├── environmentConfig.ts     # Environment variable validation
├── openai.ts                # AI caption generation
├── babecock.ts              # Image combination algorithms
├── imageAnalysis.ts         # Server-side image processing
├── imageCompression.ts      # Image optimization
├── usageTracking.ts         # Analytics and usage monitoring
├── supabase.ts              # Supabase storage client
├── upload.ts                # File upload handling (Multer)
├── errorResponse.ts         # Error response utilities
├── creditService.ts         # Credit system management
├── creditMiddleware.ts      # Credit validation middleware
└── promptThemes.ts          # AI caption theme definitions

Key Server Files

index.ts - Server Entry Point

Initializes Express, middleware, and routes:
import express from "express";
import session from "express-session";
import { setupReplitAuth, setupLocalAuth } from "./auth";
import { router as apiRoutes } from "./routes";
import { db } from "./db";
import { logger } from "./logger";

const app = express();
const PORT = 5000;

// Middleware
app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ extended: true, limit: '100mb' }));

// Session configuration
app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  store: new PostgresSessionStore({ pool: db.pool }),
  cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days
}));

// Authentication setup (Replit OIDC or local)
if (process.env.REPLIT_DOMAINS) {
  setupReplitAuth(app);
} else {
  setupLocalAuth(app);
}

// API routes
app.use('/api', apiRoutes);

// Development: Vite middleware
if (process.env.NODE_ENV === 'development') {
  const { setupViteMiddleware } = await import('./vite');
  await setupViteMiddleware(app);
}

// Production: Serve static files
else {
  app.use(express.static('dist/public'));
}

app.listen(PORT, () => {
  logger.info(`Server running on http://localhost:${PORT}`);
});

routes.ts - API Route Definitions

Defines all REST API endpoints:
import { Router } from "express";
import { storage } from "./storage";
import { isAuthenticated } from "./authUtils";
import { z } from "zod";
import { insertSessionSchema } from "@shared/schema";

export const router = Router();

// Session Management
router.get('/sessions', isAuthenticated, async (req, res) => {
  const userId = req.user!.id;
  const sessions = await storage.getUserSessions(userId);
  res.json(sessions);
});

router.post('/sessions', isAuthenticated, async (req, res) => {
  const userId = req.user!.id;
  const validated = insertSessionSchema.parse(req.body);
  const session = await storage.createSession({ ...validated, userId });
  res.json(session);
});

router.get('/sessions/:id', isAuthenticated, async (req, res) => {
  const sessionId = parseInt(req.params.id);
  const session = await storage.getSession(sessionId);
  
  if (!session) {
    return res.status(404).json({ error: 'Session not found' });
  }
  
  // Verify ownership or public access
  if (session.userId !== req.user!.id && !session.isPublic) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  res.json(session);
});

// ... more routes

storage.ts - Database Abstraction Layer

Implements the IStorage interface for all database operations:
import { db } from "./db";
import * as schema from "@shared/schema";
import { eq, and, desc } from "drizzle-orm";

interface IStorage {
  // Session CRUD
  createSession(data: InsertSession): Promise<Session>;
  updateSession(id: number, data: UpdateSession): Promise<Session>;
  deleteSession(id: number): Promise<void>;
  getSession(id: number): Promise<Session | null>;
  getUserSessions(userId: string): Promise<Session[]>;
  
  // Media Management
  addSessionMedia(data: InsertSessionMedia[]): Promise<void>;
  clearSessionMedia(sessionId: number): Promise<void>;
  getSessionMedia(sessionId: number): Promise<SessionMedia[]>;
  
  // Sharing
  shareSession(sessionId: number, userId?: string): Promise<SharedSession>;
  getSessionByShareCode(shareCode: string): Promise<Session | null>;
}

class DatabaseStorage implements IStorage {
  async createSession(data: InsertSession): Promise<Session> {
    const [session] = await db.insert(schema.contentSessions)
      .values(data)
      .returning();
    return session;
  }
  
  async getSession(id: number): Promise<Session | null> {
    const [session] = await db.select()
      .from(schema.contentSessions)
      .where(eq(schema.contentSessions.id, id));
    return session || null;
  }
  
  // ... more implementations
}

export const storage = new DatabaseStorage();

Shared Directory Structure

The shared/ directory contains code used by both client and server:
shared/
├── schema.ts            # Drizzle ORM schema and Zod validation
└── types.ts             # Shared TypeScript types

schema.ts - Central Schema Definition

Defines database schema using Drizzle ORM and Zod validation schemas:
import { pgTable, serial, text, varchar, timestamp, boolean, integer } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";

// Database table definitions
export const users = pgTable("users", {
  id: varchar("id").primaryKey().notNull(),
  email: varchar("email").unique(),
  password: varchar("password"),
  firstName: varchar("first_name"),
  lastName: varchar("last_name"),
  profileImageUrl: varchar("profile_image_url"),
  role: varchar("role").notNull().default("user"),
  isActive: boolean("is_active").notNull().default(true),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

export const contentSessions = pgTable("content_sessions", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  userId: varchar("user_id").references(() => users.id),
  subreddits: text("subreddits").array().notNull(),
  intervalMin: integer("interval_min").default(3),
  intervalMax: integer("interval_max").default(10),
  transition: text("transition").default("fade"),
  thumbnail: text("thumbnail"),
  aiPrompt: text("ai_prompt"),
  captionTheme: text("caption_theme"),
  isPublic: boolean("is_public").default(false),
  isFavorite: boolean("is_favorite").default(false),
  isManualMode: boolean("is_manual_mode").default(false),
  isImported: boolean("is_imported").default(false),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

export const sessionMedia = pgTable("session_media", {
  id: serial("id").primaryKey(),
  sessionId: integer("session_id").references(() => contentSessions.id, { onDelete: "cascade" }),
  mediaUrl: text("media_url").notNull(),
  thumbnail: text("thumbnail"),
  caption: text("caption"),
  type: text("type"),
  redditPostId: text("reddit_post_id"),
  subreddit: text("subreddit"),
  order: integer("order").notNull(),
});

// Zod validation schemas
export const insertSessionSchema = createInsertSchema(contentSessions, {
  title: z.string().min(1).max(200),
  subreddits: z.array(z.string()).min(1).max(10),
  intervalMin: z.number().min(1).max(30),
  intervalMax: z.number().min(1).max(60),
  transition: z.enum(["fade", "slide", "zoom", "flip", "none"]),
});

export const insertSessionMediaSchema = createInsertSchema(sessionMedia, {
  caption: z.string().max(500).optional(),
});

// TypeScript types
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
export type Session = typeof contentSessions.$inferSelect;
export type InsertSession = typeof contentSessions.$inferInsert;
export type SessionMedia = typeof sessionMedia.$inferSelect;
export type InsertSessionMedia = typeof sessionMedia.$inferInsert;
Single Source of Truth: The shared/schema.ts file is the single source of truth for database schema, validation rules, and TypeScript types. Both client and server import from this file.

Import Aliases

The project uses TypeScript path aliases for cleaner imports:
tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./client/src/*"],
      "@shared/*": ["./shared/*"],
      "@assets/*": ["./client/src/assets/*"]
    }
  }
}

Usage Examples

import { Button } from "@/components/ui/button";
import { insertSessionSchema } from "@shared/schema";
import logo from "@assets/logo.png";

Data Flow Architecture

Request Flow

Type Safety Flow

Build Configuration

Vite Configuration (vite.config.ts)

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./client/src"),
      "@shared": path.resolve(__dirname, "./shared"),
      "@assets": path.resolve(__dirname, "./client/src/assets"),
    },
  },
  server: {
    port: 5173,
    proxy: {
      "/api": {
        target: "http://localhost:5000",
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: "dist/public",
    emptyOutDir: true,
  },
});

Build Scripts (package.json)

{
  "scripts": {
    "dev": "NODE_ENV=development tsx server/index.ts",
    "build": "node scripts/generate-logos.mjs && vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist --minify",
    "start": "NODE_ENV=production node dist/index.js",
    "check": "tsc",
    "db:push": "drizzle-kit push"
  }
}

Styling Architecture

Tailwind + Radix UI

The application uses Tailwind CSS for utility-first styling and Radix UI for accessible component primitives:
client/src/
├── components/ui/       # Radix UI wrapper components
│   ├── button.tsx       # Styled Button component
│   ├── dialog.tsx       # Modal dialog
│   ├── dropdown-menu.tsx
│   ├── toast.tsx
│   └── ...
├── index.css            # Global styles + Tailwind imports
└── styles/              # Additional style utilities
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input hover:bg-accent",
        ghost: "hover:bg-accent hover:text-accent-foreground",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 px-3",
        lg: "h-11 px-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);

export { Button, buttonVariants };

Next Steps

Now that you understand the project structure:

Build docs developers (and LLMs) love