All docs/general

docs/architecture/admin-app-architecture.md

Admin App Architecture

Last verified: 2026-03-05 Target: apps/admin Companion: no dedicated companion package (see @repo/app-admin-library for shared admin UI components)

Overview

apps/admin is the internal operations dashboard for the Happy Client video testimonial platform. It serves superadmins and organization admins with tools to: configure video flows, manage organizations, view respondent submissions, orchestrate video processing, export content, and monitor analytics. The app runs on Next.js 15 App Router (port 3007) and is the primary ingest point for respondent video submissions via its webhook endpoint.

Key traits:

  • Server Components by default — all data-fetching happens server-side; "use client" pushed to leaf components
  • Webhook receiverPOST /api/webhooks/process-video is the central ingest point after a respondent completes a flow
  • Registry-backed data — reads/writes via Redis KV registries (@repo/registries) rather than a relational DB
  • Superadmin-first — full app access requires SUPERADMIN_ORG_IDS membership; org admins see a scoped subset

Module Overview


Primary Data Flow — Video Submission Ingest

The core flow is a respondent completing a video session, which triggers a webhook that the admin app processes:


Integration Map


Route Inventory

RoutePurposeAuth Requirement
/Auto-redirect to org dashboard or superadmin orgs listAny authenticated user
/sign-inClerk sign-in pagePublic
/sso-callbackClerk SSO callbackPublic
/organizationsList all orgs with plan tier and flow countsSuperadmin
/organizations/[orgId]Org detail: flows, members, linksOrg admin or superadmin
/organizations/[orgId]/profileEdit org name / brandingOrg admin
/organizations/[orgId]/assetsUpload logo, brand imagesOrg admin
/organizations/[orgId]/email-settingsConfigure and test email deliveryOrg admin
/organizations/[orgId]/invitationsManage invitation URLsOrg admin
/flows/[videoFlowId]Flow overview dashboard: respondent list, analyticsOrg admin
/flows/[videoFlowId]/editEdit flow config (questions, branding, theme)Org admin
/flows/[videoFlowId]/kickoffManage campaign context (USPs, messaging)Org admin
/flows/[videoFlowId]/[respondentId]Respondent detail: videos, transcriptions, clipsOrg admin
/flows/[videoFlowId]/video-flow-configuratorAdvanced BVF flow builderOrg admin
/flows/createCreate new video flowOrg admin
/video-exporterMulti-clip editor + export (FCP XML, ZIP)Org admin
/hello-draftsMonitor Hello onboarding draftsSuperadmin
/hello-drafts/[draftId]Edit a Hello draftSuperadmin
/importCSV story prompt importSuperadmin
/import/manualManual session/respondent importSuperadmin
/healthHealth check (returns 200)Public
POST /api/webhooks/process-videoReceive respondent video submissionsWebhook secret
POST /api/export-xmlGenerate Final Cut Pro XML timelineSession
POST /api/download-all-videosZIP all respondent videosSession
POST /api/download-videoDownload single videoSession
POST /api/download-srtGenerate SRT captions fileSession
POST /api/blob/uploadUpload brand assetsSession
GET /api/correlationSentry correlation IDPublic
GET /api/infoApp version infoPublic

Authentication & Authorization

The admin app uses a two-layer auth model:

Layer 1: Clerk Middleware
  - auth() checks session on every request
  - Unauthenticated → redirect to /sign-in

Layer 2: Organization Role Check (per-page)
  - getClients().organization.verifyOrganisationAdmin()
  - Checks Clerk org membership + role
  - isSuperAdmin: user belongs to one of SUPERADMIN_ORG_IDS
  - isOrgAdmin: user has 'admin' role in current org (Clerk)

SuperAdmin (SUPERADMIN_ORG_IDS membership): Full access. Can manage all orgs, see all flows, access Hello drafts.

Org Admin (Clerk admin role in org): Scoped to their org's flows and respondents. Cannot see other orgs.

Org Member (default Clerk member): Limited read-only access. Cannot perform mutations.


Service Registry Pattern

All services are lazily initialized via getClients() in app/_providers/server/clients.ts:

let _clients: Clients | null = null;

export function getClients(): Clients {
  if (_clients) return _clients;
  const env = getAdminEnvironment();
  _clients = {
    organization: createOrganizationClient({ ... }),
    videoProcessingRegistry: new VideoProcessingRegistry({ ... }),
    videoAnalyticsRegistry: new VideoAnalyticsRegistry({ ... }),
    organizationRegistry: new OrganizationRegistry({ ... }),
    // ... additional services
  };
  return _clients;
}

// Convenience accessors (avoid full getClients() when only one service needed)
export function getVideoProcessingRegistry() { return getClients().videoProcessingRegistry; }
export function getTestimonialService() { return getClients().testimonialService; }

This pattern prevents import-time environment access (which causes Next.js static analysis failures) and supports testing via mock injection.


Error Handling Patterns

Server Component Pages

// Preferred pattern (kickoff page, flows pages)
try {
  await organizationClient.verifyOrganisationAdmin();
  const data = await getVideoProcessingRegistry().getClientName(videoFlowId);
  if (!data) notFound();
  return <Component data={data} />;
} catch (error) {
  unstable_rethrow(error);   // Pass Next.js navigation errors through
  const logger = getLogger();
  logger.error("Page failed to load", { error });
  redirect("/");
}

Note: Some older pages use console.error instead of getLogger() — see improvement task arch-improvement-admin-error-logging-consistency.

API Route Handlers

// All API routes use these helpers from lib/api-validation.ts:
import { handleValidationError } from "../lib/api-validation";
import { validateApiEnvironment } from "../lib/api-runtime-validation";

export async function POST(request: NextRequest) {
  try {
    validateApiEnvironment();        // Fail fast if env misconfigured
    const body = await request.json();
    const validated = schema.parse(body);  // Zod validation
    // ... process
    return NextResponse.json({ success: true, ... });
  } catch (error) {
    const validationError = handleValidationError(error);
    if (validationError) return validationError;
    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
  }
}

Client Components

Uses useSentryToast from @repo/core/web/use-sentry-toast for error toasts that also capture to Sentry:

const { toast } = useSentryToast();
toast.error("Save failed", {
  error: error instanceof Error ? error : new Error(String(error)),
  context: { action: "save_flow_config", videoFlowId },
});

Environment Variables

Required

# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
CLERK_SECRET_KEY
SUPERADMIN_ORG_IDS          # Comma-separated Clerk org IDs with full admin access

# Service URLs
VIDEO_SERVICE_URL            # Video processor Cloud Run endpoint
HELLO_APP_URL
BRANDED_VIDEO_FLOW_URL
LIBRARY_APP_URL

# Redis KV (Upstash or compatible)
PROCESSED_VIDEO_DATA_KV_URL  # Flow configs, respondent data, video metadata
VIDEO_ANALYTICS_KV_URL       # Monthly stats, engagement metrics
ASSET_REGISTRY_KV_URL        # Brand asset metadata

# Blob Storage
BLOB_READ_WRITE_TOKEN                        # Asset storage (logos, images)
VIDEO_FLOW_VIDEOS_BLOB_READ_WRITE_TOKEN      # Respondent video files

# Webhook security
WEBHOOK_SECRET               # HMAC secret for validating process-video webhook
CRON_SECRET                  # Bearer token for Vercel cron jobs

# Email
RESEND_API_KEY

# Error Tracking
SENTRY_DSN

Optional

OPENAI_API_KEY               # Whisper STT, caption translation
OPENROUTER_API_KEY           # AI image generation in flow editor
UNSPLASH_ACCESS_KEY          # Stock image search
RESEND_TEMPLATE_ID           # Email template (falls back to inline HTML)
POSTHOG_FLOW_ANALYTICS_API_KEY / PROJECT_ID / HOST
R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET_NAME
ENABLE_DEBUG_LOGGING

Key Packages & Roles

PackageWhat Admin Uses It For
@repo/authClerk org membership checks, role verification
@repo/registriesAll Redis KV data access (flows, respondents, analytics, org profiles, CI scores)
@repo/servicesTestimonialService (video processing orchestration), RecentVideosCacheService, TestimonialNotificationService (Resend email)
@repo/videoVideoProcessingClient (FFmpeg metadata), ContentIntelligenceService, videoExportService, caption translation
@repo/storageBlob upload/delete (brand assets, video files), Cloudflare R2 support
@repo/app-video-flowFlowConfigSchema, buildFlowUrl(), flow client helpers
@repo/app-hellobuildDraftFromFlowConfig(), UnsplashApiClient
@repo/app-admin-libraryVideoProjectComposer (multi-clip video editor UI component)
@repo/app-libraryextractPreloadThumbnails() for server-side image preloading
@repo/coreLogger, useSentryToast, VercelAnalytics, environment utilities
@repo/design-systemRadix UI + Tailwind components (Button, Card, Badge, Toaster, etc.)

Security

Webhook Authentication

POST /api/webhooks/process-video requires a valid X-Webhook-Signature HMAC header matching WEBHOOK_SECRET. Invalid signatures return 401.

Content Security Policy

Strict CSP in next.config.js covering:

  • script-src: Self + Clerk + Sentry + PostHog CDNs
  • img-src: Self, data, blob + Vercel/AWS/Cloudflare CDN domains + Unsplash/Pexels
  • media-src: Self, blob, CDN domains + HLS streaming hosts
  • object-src: None (block plugins)

Standard Security Headers

Strict-Transport-Security: max-age=63072000
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(self), microphone=(self)

Caching

CacheMechanismTTL
Hello drafts listunstable_cache + revalidateTag("hello-drafts")30s
Org membershipIn-memory _orgCache with TTL guard60s
Recent videosRecentVideosCacheService (Redis-backed)On-demand invalidation
All pagesexport const dynamic = "force-dynamic"No HTTP cache

Build & Deployment

  • Output: Next.js standalone (output: "standalone" in next.config.js)
  • Port: 3007 (port formula: 3000 + PORT_OFFSET + 7)
  • Transpiled workspace packages: @repo/auth, @repo/design-system, @repo/video, @repo/storage, @repo/app-video-flow, @repo/app-admin-library, @repo/core, @repo/services
  • Sentry: Source maps auto-uploaded on build when SENTRY_AUTH_TOKEN is set
  • Deployments: GitHub CI only — never vercel deploy manually