Admin App Architecture
Last verified: 2026-03-05 Target:
apps/adminCompanion: no dedicated companion package (see@repo/app-admin-libraryfor 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 receiver —
POST /api/webhooks/process-videois 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_IDSmembership; 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
| Route | Purpose | Auth Requirement |
|---|---|---|
/ | Auto-redirect to org dashboard or superadmin orgs list | Any authenticated user |
/sign-in | Clerk sign-in page | Public |
/sso-callback | Clerk SSO callback | Public |
/organizations | List all orgs with plan tier and flow counts | Superadmin |
/organizations/[orgId] | Org detail: flows, members, links | Org admin or superadmin |
/organizations/[orgId]/profile | Edit org name / branding | Org admin |
/organizations/[orgId]/assets | Upload logo, brand images | Org admin |
/organizations/[orgId]/email-settings | Configure and test email delivery | Org admin |
/organizations/[orgId]/invitations | Manage invitation URLs | Org admin |
/flows/[videoFlowId] | Flow overview dashboard: respondent list, analytics | Org admin |
/flows/[videoFlowId]/edit | Edit flow config (questions, branding, theme) | Org admin |
/flows/[videoFlowId]/kickoff | Manage campaign context (USPs, messaging) | Org admin |
/flows/[videoFlowId]/[respondentId] | Respondent detail: videos, transcriptions, clips | Org admin |
/flows/[videoFlowId]/video-flow-configurator | Advanced BVF flow builder | Org admin |
/flows/create | Create new video flow | Org admin |
/video-exporter | Multi-clip editor + export (FCP XML, ZIP) | Org admin |
/hello-drafts | Monitor Hello onboarding drafts | Superadmin |
/hello-drafts/[draftId] | Edit a Hello draft | Superadmin |
/import | CSV story prompt import | Superadmin |
/import/manual | Manual session/respondent import | Superadmin |
/health | Health check (returns 200) | Public |
POST /api/webhooks/process-video | Receive respondent video submissions | Webhook secret |
POST /api/export-xml | Generate Final Cut Pro XML timeline | Session |
POST /api/download-all-videos | ZIP all respondent videos | Session |
POST /api/download-video | Download single video | Session |
POST /api/download-srt | Generate SRT captions file | Session |
POST /api/blob/upload | Upload brand assets | Session |
GET /api/correlation | Sentry correlation ID | Public |
GET /api/info | App version info | Public |
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
| Package | What Admin Uses It For |
|---|---|
@repo/auth | Clerk org membership checks, role verification |
@repo/registries | All Redis KV data access (flows, respondents, analytics, org profiles, CI scores) |
@repo/services | TestimonialService (video processing orchestration), RecentVideosCacheService, TestimonialNotificationService (Resend email) |
@repo/video | VideoProcessingClient (FFmpeg metadata), ContentIntelligenceService, videoExportService, caption translation |
@repo/storage | Blob upload/delete (brand assets, video files), Cloudflare R2 support |
@repo/app-video-flow | FlowConfigSchema, buildFlowUrl(), flow client helpers |
@repo/app-hello | buildDraftFromFlowConfig(), UnsplashApiClient |
@repo/app-admin-library | VideoProjectComposer (multi-clip video editor UI component) |
@repo/app-library | extractPreloadThumbnails() for server-side image preloading |
@repo/core | Logger, useSentryToast, VercelAnalytics, environment utilities |
@repo/design-system | Radix 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 CDNsimg-src: Self, data, blob + Vercel/AWS/Cloudflare CDN domains + Unsplash/Pexelsmedia-src: Self, blob, CDN domains + HLS streaming hostsobject-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
| Cache | Mechanism | TTL |
|---|---|---|
| Hello drafts list | unstable_cache + revalidateTag("hello-drafts") | 30s |
| Org membership | In-memory _orgCache with TTL guard | 60s |
| Recent videos | RecentVideosCacheService (Redis-backed) | On-demand invalidation |
| All pages | export const dynamic = "force-dynamic" | No HTTP cache |
Build & Deployment
- Output: Next.js standalone (
output: "standalone"innext.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_TOKENis set - Deployments: GitHub CI only — never
vercel deploymanually