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
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 16.0.8 | Framework (App Router, SSR, API routes) |
| React | 19.2.1 | UI library |
| TypeScript | 5.6+ | Static typing |
| Redux Toolkit | 2.9.0 | Client-side state (post creation, UI) |
| React Redux | 9.2.0 | React bindings for Redux |
| TanStack React Query | 5.59+ | Server state, caching, persistence |
| Tailwind CSS | ~3.4 | Utility-first CSS |
| Storybook | 10.2 | Component development and documentation |
| Firebase Client SDK | 10.12.5 | Auth, Firestore, Storage |
| Firebase Admin SDK | 12.5.0 | Server-side auth verification |
| Stripe | 18.4.0 (JS), 7.8.0 (@stripe/stripe-js) | Billing and payments |
| @xyflow/react | 12.8.4 | Automation workflow editor |
| Radix UI | Various | Accessible primitive components |
| Framer Motion / Motion | 12.x | Animations |
| Konva / react-konva | 9.x / 19.x | Canvas-based media editor |
| Lucide React | 0.539+ | Icons |
| Jest | 29.7 | Unit 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
localStoragevia@tanstack/query-sync-storage-persister, so cached data survives page reloads
App Router Structure
Public Routes (No Auth Required)
| Route | Description |
|---|---|
/ | Landing page |
/signin | Sign-in page (email link + Google) |
/signup | Sign-up page |
/pricing | Pricing plans |
/privacy-policy | Privacy policy |
/terms-of-service | Terms of service |
/affiliates | Affiliate program |
/success | Post-checkout success |
Protected Routes (Auth Required)
These routes are guarded by middleware.js and require a valid __session cookie:
| Route | Description |
|---|---|
/dashboard | Main dashboard with post overview and analytics |
/create | Post creation flow (media upload, caption, platform selection) |
/posts | Published and scheduled post listings |
/templates | Content templates for reuse |
/automations | Workflow automation builder (@xyflow/react) |
/settings | User settings, connected accounts |
/admin | Staff-only admin panel |
/api-dashboard | Public API key management and usage |
/billing | Subscription management |
/library | Media asset library |
API Routes (/api/)
Server-side route handlers in app/api/:
| Route Group | Endpoints | Purpose |
|---|---|---|
auth/session | POST | Mints __session cookie via Admin SDK createSessionCookie() |
auth/subscription | GET | Reads subscription status from Firestore |
auth/subscription-check | GET | Lightweight subscription check |
api-keys/ | CRUD | Manages public API keys |
api-requests/ | GET | API usage history |
billing/change-plan | POST | Subscription plan changes |
billing/portal | POST | Stripe Customer Portal session |
create-checkout-session/ | POST | Creates Stripe Checkout session |
credits/balance | GET | AI credit balance |
debug/ | Various | Development debugging endpoints |
firstpromoter/ | Various | Affiliate tracking webhooks |
library/assets | GET | Library asset listing |
library/folders | CRUD | Library folder management |
library/upload-url | POST | Signed upload URL generation |
verify-session/ | POST | Session cookie verification |
webhooks/firstpromoter | POST | FirstPromoter 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. CheckshasActiveSubfromAuthContextand 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 usingnext-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
Userobject, loading state, claims-loaded flag - Subscription state:
hasActiveSub,subscriptionobject,subscriptionStatus,tier - Feature gating:
hasFeature(feature),getLimit(limitKey),canAdd(limitKey, count)— all driven by@soku/schematier definitions - Staff detection:
isStaffflag 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
- Sign-in: User authenticates via email link (passwordless) or Google OAuth through Firebase Auth on the client.
- Session creation: After successful auth, the client calls
POST /api/auth/sessionwith the Firebase ID token. The API route verifies the token via Admin SDK and creates an HTTP-only session cookie (__session) usingadmin.auth().createSessionCookie(). - Custom claims: The session cookie embeds custom claims set by the backend:
hasActiveSub,subscriptionStatus,tier, andisStaff. - Route protection:
middleware.jsruns on protected routes and checks for the presence of the__sessioncookie. If missing, the user is redirected to/signin. - Email link exception: The middleware allows through requests with
mode=signInormode=signInViaEmailLinkplus anoobCodeparameter, 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
clsxfor conditional class compositiontailwind-mergeto resolve conflicting Tailwind classesclass-variance-authority(CVA) for component variant definitions incomponents/ui/- Mobile-first responsive design using Tailwind breakpoints
Development Tools
- Storybook 10: Run
pnpm --filter web storybookto launch the component explorer at port 6006. Includes@storybook/addon-a11yfor accessibility checks and@storybook/addon-docsfor documentation. - React Query Devtools: Included in development builds, accessible via a floating button in the bottom-left corner.
- Jest: Run
pnpm --filter web testfor unit tests. Configured withts-jestfor TypeScript support. - ESLint 9: Run
pnpm --filter web lint. Configured witheslint-config-next,eslint-config-prettier, andeslint-plugin-security.