Architecture

Stack overview

OpsDash is a production-ready, multi-tenant B2B platform built on Next.js 15, Supabase, and Stripe. It implements the complete Lead → Deal → Project → Profitability loop in a single, privately deployable codebase.

Stack

LayerTechnology
FrameworkNext.js 15 — App Router, React Server Components, Server Actions
LanguageTypeScript 5.7
DatabaseSupabase — PostgreSQL 15 + Auth + Realtime + Storage
StylingTailwind CSS 3 + Radix UI (@opsdash/ui)
BillingStripe — subscriptions, entitlements, webhooks
i18nnext-intl
ValidationZod
MonorepoTurborepo + pnpm workspaces

Monorepo structure

/
  apps/
    web/          # Next.js app — the main product
    docs/         # Storybook component docs
  packages/
    @opsdash/ui           # Design system (Radix, TanStack Table, CVA)
    @opsdash/database     # Supabase client wrappers
    @opsdash/auth         # Auth helpers
    @opsdash/utils        # formatDate, clsx, Zod, PDF export
    @opsdash/i18n         # next-intl wrapper
    @opsdash/config       # Feature defaults, plan limits, config types
    @opsdash/crm          # CRM UI components
    @opsdash/projects     # Projects UI components
    @opsdash/forms        # Form builder UI
    @opsdash/billing      # Stripe + billing UI
    @opsdash/analytics    # Charts (recharts) + analytics UI
    @opsdash/ai           # AI-related UI components
  supabase/
    migrations/   # 57 SQL migration files (additive only)
  opsdash.config.ts  # Buyer entry point

Route structure

All authenticated pages sit under /[locale]/org/[slug]/. Locale and org slug are resolved by middleware on every request.

/[locale]/
  org/[slug]/
    dashboard/         # KPIs and activity feed
    analytics/         # Charts and trends
    reports/           # Pipeline, deal conversion, forecasting
    crm/
      contacts/        # Contact list + detail
      companies/       # Company list + detail
      leads/           # Lead list + detail
      deals/           # Pipeline board + deal detail
      activities/      # CRM activity feed
      automation/      # Workflow triggers
      export/          # Data export
      data-cleanup/    # Go-live checklist + duplicate merge (see file-map.md → crm/data-cleanup/)
    projects/          # Project list
    projects/[id]/     # Kanban, calendar, gantt, team
    support/           # Ticket list + detail
    forms/             # Form builder
    files/             # File manager
    billing/           # Subscription management
    settings/          # Org settings
  account/             # Profile, 2FA, billing anchor
  admin/               # Platform admin (PLATFORM_ADMIN_EMAILS only)

Backend: Server Actions

There is no traditional REST API for product data. All CRUD goes through Next.js Server Actions in apps/web/src/app/actions/. The security pattern applied to every action:

const { supabase, org, user } = await withOrg(slug);
// withOrg() verifies auth, resolves org, checks membership — throws on failure
const parsed = schema.safeParse(input);
if (!parsed.success) return { success: false, error: 'validation_error' };
// ... data operation ...
await createAuditLog(supabase, org.id, user.id, 'entity.action', { ... });
return { success: true, data };

Data flow

  1. Browser requests a page → React Server Component renders server-side
  2. RSC calls a Server Action to fetch data → withOrg() validates auth and org membership
  3. Supabase Row-Level Security enforces org_id at the DB layer — even buggy server actions cannot leak cross-org data
  4. For mutations: Client Component calls a Server Action → same withOrg() + Zod validation → DB write + audit log

Feature access stack

Every protected feature passes through four layers before data is returned:

  1. Entitlement check — is the feature included in the org's Stripe plan?
  2. Subscription status — must be active or trialing
  3. Feature flagfeature_flags table — is it enabled for this org?
  4. Role check — is the user's role in feature_flags.allowed_roles?
FunctionLocationPurpose
canAccessFeature()org-context.tsxClient: sidebar, command palette, route guards
requireFeatureAccess()admin.ts → admin-feature-gates.tsServer actions: returns error if no access
ensureFeatureAccess()admin.ts → admin-feature-gates.tsLayouts: redirects to dashboard if no access
checkFeature()admin.ts → admin-feature-gates.tsSubscription/entitlement check only (no role)