Skip to main content

Web App Architecture

The Soku web application lives in apps/web/ and is the primary user-facing product. It is a Next.js 16 application using the App Router, React 19, Redux Toolkit for client state, TanStack React Query v5 for server state, and Tailwind CSS for styling.

Technology Overview

TechnologyVersionPurpose
Next.js16.0.8Framework (App Router, SSR, API routes)
React19.2.1UI library
TypeScript5.6+Static typing
Redux Toolkit2.9.0Client-side state (post creation, UI)
React Redux9.2.0React bindings for Redux
TanStack React Query5.59+Server state, caching, persistence
Tailwind CSS~3.4Utility-first CSS
Storybook10.2Component development and documentation
Firebase Client SDK10.12.5Auth, Firestore, Storage
Firebase Admin SDK12.5.0Server-side auth verification
Stripe18.4.0 (JS), 7.8.0 (@stripe/stripe-js)Billing and payments
@xyflow/react12.8.4Automation workflow editor
Radix UIVariousAccessible primitive components
Framer Motion / Motion12.xAnimations
Konva / react-konva9.x / 19.xCanvas-based media editor
Lucide React0.539+Icons
Jest29.7Unit testing
Zod (via @soku/schema)3.22+Shared validation schemas

Source Directory Structure

apps/web/src/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout (fonts, providers, AppShell)
│ ├── page.tsx # Landing page (/)
│ ├── providers.tsx # Client provider tree
│ ├── middleware.js # Auth middleware
│ ├── globals.css # Tailwind base + global styles
│ ├── GTM.tsx # Google Tag Manager
│ ├── error.tsx # Global error boundary
│ ├── not-found.tsx # 404 page
│ ├── admin/ # Admin dashboard
│ ├── affiliates/ # Affiliate program page
│ ├── api/ # API route handlers
│ ├── api-dashboard/ # Public API dashboard
│ ├── automations/ # Workflow automations
│ ├── billing/ # Subscription management
│ ├── create/ # Post creation flow
│ ├── dashboard/ # Main dashboard
│ ├── library/ # Media library
│ ├── posts/ # Published posts
│ ├── pricing/ # Pricing page (public)
│ ├── privacy-policy/ # Privacy policy (public)
│ ├── settings/ # User settings
│ ├── signin/ # Sign-in page
│ ├── signup/ # Sign-up page
│ ├── success/ # Post-checkout success
│ ├── templates/ # Content templates
│ └── terms-of-service/ # Terms (public)
├── components/ # React components
│ ├── shared/ # Cross-feature shared components
│ ├── ui/ # Design system primitives (buttons, dialogs, etc.)
│ ├── dashboard/ # Dashboard-specific components
│ ├── create/ # Post creation components
│ ├── templates/ # Template components
│ ├── automations/ # Automation workflow components
│ ├── integrations/ # Integration management components
│ ├── library/ # Media library components
│ ├── posts/ # Post listing components
│ ├── settings/ # Settings components
│ ├── subscription/ # Subscription/billing components
│ ├── api-dashboard/ # API dashboard components
│ ├── LandingPage/ # Landing page sections
│ ├── AppShell.tsx # Layout shell (sidebar + content area)
│ ├── Sidebar.tsx # Navigation sidebar
│ ├── Modal.tsx # Modal component
│ ├── Stepper.tsx # Step indicator
│ ├── SubscriptionGuard.tsx # Paywall gate component
│ ├── ReduxFirebaseSync.tsx # Syncs Firebase state to Redux
│ └── ThemeToggle.tsx # Dark/light mode toggle
├── context/ # React Context providers
│ ├── AuthContext.tsx # Authentication state + methods
│ └── IntegrationsContext.tsx # Connected platform accounts
├── hooks/ # Custom React hooks (by feature)
│ ├── auth/ # Auth-related hooks
│ ├── create/ # Post creation hooks
│ ├── billing/ # Billing/subscription hooks
│ ├── automations/ # Automation hooks
│ ├── apiDashboard/ # API dashboard hooks
│ ├── dashboard/ # Dashboard data hooks
│ ├── integrations/ # Integration hooks
│ ├── library/ # Media library hooks
│ ├── published/ # Published post hooks
│ ├── scheduled/ # Scheduled post hooks
│ ├── settings/ # Settings hooks
│ └── templates/ # Template hooks
├── store/ # Redux Toolkit store
│ ├── index.ts # Store configuration + typed hooks
│ ├── slices/
│ │ ├── createPostSlice.ts # Post creation state
│ │ └── uiSlice.ts # UI state (sidebar, theme, feature flags)
│ ├── selectors/
│ │ ├── createPostSelectors.ts
│ │ └── index.ts
│ └── hooks/
│ └── index.ts
├── utils/ # Utility modules
│ ├── firebaseClient.ts # Firebase client SDK initialization + auth helpers
│ ├── firebaseAdmin.ts # Firebase Admin SDK (server-side only)
│ ├── firestore.ts # Firestore client helpers
│ ├── stripe.ts # Stripe client utilities
│ ├── stripe-server.ts # Stripe server-side utilities
│ ├── stripe-customer.js # Stripe customer management
│ ├── publish.ts # Publish flow utilities
│ ├── publishInternal.ts # Internal publish helpers
│ ├── authStart.ts # OAuth initiation helpers
│ ├── integrationTargets.ts # Platform target configuration
│ ├── targets.ts # Publish target helpers
│ ├── tiktok.ts # TikTok-specific utilities
│ ├── dates.ts # Date formatting utilities
│ ├── firstpromoter.ts # Affiliate tracking
│ └── automations/ # Automation-specific utils
└── middleware.js # Next.js edge middleware

Provider Architecture

The root layout (layout.tsx) wraps the application in a provider tree defined in providers.tsx. The nesting order matters because inner providers depend on outer ones:

ThemeProvider                    ← next-themes (dark/light mode)
└─ ReduxProvider ← Redux Toolkit store
└─ PersistQueryClientProvider ← React Query + localStorage persistence
└─ AuthProvider ← Firebase auth state + custom claims
└─ ReduxFirebaseSync ← Syncs auth state into Redux
└─ IntegrationsProvider ← Connected social accounts
└─ AppShell ← Sidebar + content layout
└─ {children} ← Page content

On the server (SSR), PersistQueryClientProvider falls back to a plain QueryClientProvider since localStorage is not available. The ReactQueryDevtools panel is included in development builds.

React Query Configuration

The query client is configured with:

  • Stale time: 5 minutes (queries are considered fresh for 5 minutes)
  • Garbage collection time: 24 hours (unused cache entries persist for a full day)
  • Persistence: Query cache is persisted to localStorage via @tanstack/query-sync-storage-persister, so cached data survives page reloads

App Router Structure

Public Routes (No Auth Required)

RouteDescription
/Landing page
/signinSign-in page (email link + Google)
/signupSign-up page
/pricingPricing plans
/privacy-policyPrivacy policy
/terms-of-serviceTerms of service
/affiliatesAffiliate program
/successPost-checkout success

Protected Routes (Auth Required)

These routes are guarded by middleware.js and require a valid __session cookie:

RouteDescription
/dashboardMain dashboard with post overview and analytics
/createPost creation flow (media upload, caption, platform selection)
/postsPublished and scheduled post listings
/templatesContent templates for reuse
/automationsWorkflow automation builder (@xyflow/react)
/settingsUser settings, connected accounts
/adminStaff-only admin panel
/api-dashboardPublic API key management and usage
/billingSubscription management
/libraryMedia asset library

API Routes (/api/)

Server-side route handlers in app/api/:

Route GroupEndpointsPurpose
auth/sessionPOSTMints __session cookie via Admin SDK createSessionCookie()
auth/subscriptionGETReads subscription status from Firestore
auth/subscription-checkGETLightweight subscription check
api-keys/CRUDManages public API keys
api-requests/GETAPI usage history
billing/change-planPOSTSubscription plan changes
billing/portalPOSTStripe Customer Portal session
create-checkout-session/POSTCreates Stripe Checkout session
credits/balanceGETAI credit balance
debug/VariousDevelopment debugging endpoints
firstpromoter/VariousAffiliate tracking webhooks
library/assetsGETLibrary asset listing
library/foldersCRUDLibrary folder management
library/upload-urlPOSTSigned upload URL generation
verify-session/POSTSession cookie verification
webhooks/firstpromoterPOSTFirstPromoter webhook handler

Component Architecture

Component Organization

Components are organized by feature domain. Shared primitives live in components/ui/ (design system) and components/shared/ (cross-feature). Feature-specific components live in their own directories.

The AppShell component provides the top-level layout structure: a sidebar for navigation and a content area for the current page. The Sidebar component handles navigation links, user info, and the mobile sidebar toggle.

Key Components

  • SubscriptionGuard: Wraps content that requires an active subscription. Checks hasActiveSub from AuthContext and either renders children or shows an upgrade prompt.
  • ReduxFirebaseSync: Listens to Firebase auth state changes and syncs relevant data into the Redux store, keeping the two state systems in agreement.
  • ThemeToggle: Switches between light, dark, and system themes using next-themes.

Design System (components/ui/)

Built on Radix UI primitives with Tailwind CSS styling. Uses class-variance-authority (CVA) for component variants and clsx + tailwind-merge for class composition. Components include dialogs, dropdown menus, tooltips, tabs, and other accessible primitives.

State Management

State is split across four systems, each with a clear responsibility:

Redux Toolkit (Client State)

Configured in store/index.ts with two slices:

createPostSlice manages the post creation workflow:

  • Content type (text, image, video)
  • Caption text and per-platform separate captions
  • Platform selection (TikTok, Instagram, Threads, YouTube, Facebook, LinkedIn, X)
  • Media files, preview URLs, upload progress
  • Upload state, publishing state, draft-saving state
  • Transcript text and AI processing state
  • TikTok-specific options (privacy, duet, stitch, commercial disclosure)
  • Scheduling options (date, time, timezone)

The middleware is configured to ignore serializable checks for File[] objects stored in createPost.mediaFiles.

uiSlice manages UI-level concerns:

  • Mobile sidebar open/closed state
  • Theme mode (light, dark, system)
  • Feature flags (aiCoach, inspiration, calendar, newPricing, emailPasswordSignin, automationTemplates)

Typed hooks (useAppDispatch, useAppSelector) are exported from store/index.ts and used throughout the application instead of raw useDispatch/useSelector.

React Query (Server State)

TanStack React Query v5 handles all server data fetching. Custom hooks in hooks/ encapsulate query logic per feature. The query cache persists to localStorage so that navigating between pages or refreshing the browser does not require re-fetching data that is still within its stale window.

AuthContext (Authentication State)

context/AuthContext.tsx provides the authentication layer:

  • User state: Firebase User object, loading state, claims-loaded flag
  • Subscription state: hasActiveSub, subscription object, subscriptionStatus, tier
  • Feature gating: hasFeature(feature), getLimit(limitKey), canAdd(limitKey, count) — all driven by @soku/schema tier definitions
  • Staff detection: isStaff flag from custom claims
  • Auth methods: sendPasswordlessLink, signUpWithEmailPassword, signInWithEmailPassword, signInWithGoogleOAuth, sendPasswordReset, signOutUser, refreshClaims

The provider listens to onAuthStateChanged and onIdTokenChanged from Firebase Auth, reads custom claims from the ID token, and subscribes to the user's subscription document in Firestore via onSnapshot for real-time updates.

IntegrationsContext (Platform Connections)

context/IntegrationsContext.tsx manages the state of connected social platform accounts. It subscribes to the user's integrations collection in Firestore and provides the current connection status for each platform (Facebook, Instagram, Threads, YouTube, TikTok, X, LinkedIn, Snapchat).

Middleware and Auth Flow

Authentication Flow

  1. Sign-in: User authenticates via email link (passwordless) or Google OAuth through Firebase Auth on the client.
  2. Session creation: After successful auth, the client calls POST /api/auth/session with the Firebase ID token. The API route verifies the token via Admin SDK and creates an HTTP-only session cookie (__session) using admin.auth().createSessionCookie().
  3. Custom claims: The session cookie embeds custom claims set by the backend: hasActiveSub, subscriptionStatus, tier, and isStaff.
  4. Route protection: middleware.js runs on protected routes and checks for the presence of the __session cookie. If missing, the user is redirected to /signin.
  5. Email link exception: The middleware allows through requests with mode=signIn or mode=signInViaEmailLink plus an oobCode parameter, since these are Firebase email-link callback URLs.

Middleware Configuration

The middleware runs only on these route patterns:

/api-dashboard/:path*
/dashboard/:path*
/create/:path*
/posts/:path*
/templates/:path*
/automations/:path*
/settings/:path*
/admin/:path*

The middleware is deliberately lightweight. It only checks cookie presence, not subscription status. Subscription checks happen in two places: on the frontend via AuthContext (for UI gating) and on backend API endpoints via dedicated authorization middleware (for security). This avoids the 700-2000ms overhead that server-side subscription verification would add to every page load.

Key Patterns and Conventions

Hook Organization by Feature

Custom hooks are organized into feature directories under hooks/. Each directory contains hooks specific to that feature's data fetching, mutations, and business logic. This keeps hook files discoverable and co-located with their feature.

Typed Redux Hooks

The store exports useAppDispatch and useAppSelector as pre-typed versions of the standard Redux hooks. All components use these instead of importing from react-redux directly:

import { useAppSelector, useAppDispatch } from '@/store';

const caption = useAppSelector((state) => state.createPost.caption);
const dispatch = useAppDispatch();

Selectors

Redux selectors live in store/selectors/ and are used for derived state. createPostSelectors.ts contains selectors for the post creation flow, keeping computation logic out of components.

Path Aliases

The web app uses the @/ path alias (configured in tsconfig.json) for imports from src/:

import { useAppSelector } from '@/store';
import { AuthProvider } from '@/context/AuthContext';
import SomeComponent from '@/components/shared/SomeComponent';

Schema-Driven Validation

The web app imports Zod schemas from @soku/schema for type-safe validation. Tier definitions, feature flags, and limits are defined in the schema package and consumed by AuthContext to gate features based on the user's subscription tier.

Styling Conventions

  • Tailwind CSS for all styling; no CSS modules or inline styles
  • clsx for conditional class composition
  • tailwind-merge to resolve conflicting Tailwind classes
  • class-variance-authority (CVA) for component variant definitions in components/ui/
  • Mobile-first responsive design using Tailwind breakpoints

Development Tools

  • Storybook 10: Run pnpm --filter web storybook to launch the component explorer at port 6006. Includes @storybook/addon-a11y for accessibility checks and @storybook/addon-docs for documentation.
  • React Query Devtools: Included in development builds, accessible via a floating button in the bottom-left corner.
  • Jest: Run pnpm --filter web test for unit tests. Configured with ts-jest for TypeScript support.
  • ESLint 9: Run pnpm --filter web lint. Configured with eslint-config-next, eslint-config-prettier, and eslint-plugin-security.