Appearance
Status note (2026-09-25): Dated implementation and operations record. Verify its request paths, permissions and provider behavior against current repository source and tests before treating them as current.
Goal
TL;DR: BloxClips Support is a thin authenticated shell around Whop Support Chats (BloxClips owns auth/RBAC/routing, Whop owns the chat itself). Day-to-day guide up top, migration history below — verify request paths against current source and tests.
BloxClips Support is a thin BloxClips-authenticated shell around Whop Support Chats. Whop owns the support feed, messages, media, composer, and real-time delivery; BloxClips owns authentication, RBAC, identity mapping, routing, and the staff inbox shell.
Current operational guide
This document is the day-to-day guide for the shipped implementation. The older migration notes below are historical context, not a work queue.
Request paths
text
clipper -> POST /api/support-chat/channel -> ensureWhopIdentity
-> supportChannels.create -> ChatSession + ChatElement
staff -> GET /api/support-chat/staff/channels?status=all
-> supportChannels.list(view=admin) -> identity enrichment
-> GET /staff/channels/:channelId -> ChatSession + ChatElement
Whop signed chat webhook -> /api/webhooks/whop -> inbox version
-> existing /api/live/events SSE -> browser refetches the staff listImplemented behavior
src/lib/whopSupportChat.tsis the only support integration boundary.WhopIdentityis a one-to-one BloxClips mapping; messages are never stored locally.- Provisioning is serialized per user, reconciles by metadata or exact title, and retries lookup after Whop's duplicate-name error.
- Staff rows are sanitized and enriched from
WhopIdentity -> WebUser. - The UI loads
status=all; Open/Resolved tabs were removed because Whop's open filter produced an empty/misleading inbox. Rows still show native status (resolved_at === nullmeans open). - Whop orders by
last_post_sent_atdescending. Search is page-local. - ChatElement
messageSentreorders sent conversations immediately. A signed Whop message webhook triggers the existing SSE invalidation for received activity. No message payload is relayed through BloxClips. dms:readremains required because ChatElement performs viewed-state calls.
Limits and deferred work
- Empty-channel filtering uses
last_message_at !== null, the only efficient bulk signal exposed by the current Support Channels list contract. - Resolve/reopen mutation controls are not wired.
resolved_atremains native Whop state; resolving never creates or deletes a conversation. - Reliable admin unread counts/last-viewed cursors are not exposed, so no local message store is being recreated to manufacture them.
- Needs-response derivation and deep links are deferred.
Verification checklist
- Static: backend focused support test and frontend support lint.
- Runtime with valid Whop configuration: new clipper, duplicate identity reconciliation, two staff accounts, same-feed selection, sent reorder, webhook/SSE refresh, resolved-row visibility, invalid-feed rejection, and friendly auth/Whop failure states.
- Required backend variables:
WHOP_API_KEY,WHOP_COMPANY_ID(biz_...),WHOP_WEBHOOK_SECRET; staff also need native Whop Support access.
Non-negotiable boundaries
Do not add custom message persistence, polling, DOM scraping, browser-supplied feed IDs, campaign-specific channels, or a second realtime chat transport. Whop owns the conversation, history, composer, attachments, realtime delivery, and exposed read behavior.
The remaining sections are audit and migration reference material, not pending implementation phases.
Product invariants
- One BloxClips user maps to one permanent Whop identity.
- One clipper has one global BloxClips Whop Support Channel, regardless of campaigns, submissions, groups, or payouts.
- Google OAuth remains BloxClips login. There is no Whop OAuth or manual Whop connection flow.
- Staff do not call
supportChannels.createfor themselves. They list customer feeds and open a selectedfeed_.... - BloxClips never stores support messages or replicas of Whop message history.
Current architecture discovered
The initial audit found an already partly-completed replacement. The original custom implementation had SupportConversation and SupportMessage Prisma tables plus separate customer and admin REST routes, custom bubbles/composers, database polling, and SSE event streams. It had already been removed from source and its tables were dropped by the prior development migration, but stale documentation described it as active.
The remaining transitional Whop implementation used WhopChatIdentity, enrollment status/failure fields, a migrateWhopChatIdentities.ts script, and an admin-only legacy Whop identity branch (admin_identity_missing). The shared /dashboard/support Whop UI and its staff inbox were already present but needed to be made permanent and consistent with automatic provisioning.
Legacy components to delete
scripts/migrateWhopChatIdentities.tsand its package script.WhopChatIdentitystatus, retry, failure, and admin-exemption logic.resolveWhopUserId,admin_identity_missing, and all migration-only documentation.- Stale support/SSE/polling documentation. The old source routes and custom UI were already absent; no compatibility routes remain.
Current Whop-related implementation
Backend src/lib/whopSupportChat.ts owns the only integration boundary. It ensures a WhopIdentity, creates the caller-owned support channel, mints tokens, lists staff channels, and retrieves a selected staff channel. src/api/routes/supportChat.ts derives identity exclusively from authenticated req.user.
Frontend surfaces/content-rewards/screens/support/SupportScreen.tsx uses the official React Elements, ChatSession, and ChatElement components with loadWhopElements. It owns the surrounding staff inbox list and page layout only.
Confirmed Whop API behavior
Official Whop documentation reviewed on 2026-08-31:
new WhopClient({ token: process.env.WHOP_API_KEY })is the server SDK client.companies.create({ parent_company_id, email, title, metadata })enrolls a connected account. Persist the returnedowner_user.idas the Whop user ID.accessTokens.create({ company_id, user_id, scoped_actions, expires_at })creates a short-lived account-scoped token. Support chat scopes aresupport_chat:read,support_chat:message:create, anddms:read(required by ChatElement viewed-state calls).supportChannels.create({ company_id, user_id })returns the existing customer support channel when one already exists. It needs the API-key permissionsupport_chat:create.supportChannels.list({ company_id, view: 'admin', order: 'last_post_sent_at', direction: 'desc', first, after })supplies the staff inbox;supportChannels.retrieve({ id })validates a selected feed.- Support channels are one customer-to-account conversations, team members with support access manage them, and IDs start with
feed_. - React initialization is
const elements = loadWhopElements()then<Elements><ChatSession token={getToken}><ChatElement options= /></ChatSession></Elements>. A callback token is refreshed byChatSessionwhen needed.
Authentication architecture
Google OAuth creates/authenticates WebUser. The authenticated backend request calls ensureWhopIdentity(req.user), then accessTokens.create with the stored Whop user ID. Only the token crosses to the browser. WHOP_API_KEY remains backend-only; requests never accept a browser-provided Whop user ID or email.
Permanent Whop identity model
WhopIdentity is a one-to-one child of WebUser, with unique connectedCompanyId and whopUserId. It is a BloxClips domain mapping, not an OAuth account and not chat message storage. The final development migration renames the previous table and drops all status/failure bookkeeping.
Automatic provisioning flow
ensureWhopIdentity returns an existing mapping first. Otherwise it requires BloxClips-authenticated, verified, syntactically valid profile email; serializes work for that WebUser with a PostgreSQL transaction-scoped advisory lock; reconciles any existing Whop connected account by its metadata.internal_user_id or exact title fallback; creates one if absent; and retries reconciliation after Whop's duplicate-name error. It persists the returned Whop owner user ID. A failed transaction leaves no partial local state and a later request reconciles Whop before creating again.
This exact flow applies to clippers, admins, and support staff. BloxClips RBAC decides staff access independently.
Clipper support flow
POST /api/support-chat/channel derives the caller from the auth cookie, ensures their identity, and calls supportChannels.create for that Whop user. The returned feed_... is rendered at /dashboard/support through ChatElement. Whop's idempotent support-channel behavior enforces the single global conversation.
Staff support flow
GET /api/support-chat/staff/channels requires BloxClips staff authorization, ensures the staff user's Whop identity, and lists BloxClips support feeds. It never creates a channel for that staff user. Selecting a conversation calls the RBAC-protected GET /api/support-chat/staff/channels/:channelId, which verifies that the feed belongs to the configured Whop account before ChatElement receives it. Whop native team/participant access remains the final chat authorization.
Whop staff must be configured as team members with native support access (moderator is sufficient according to Whop's Support Chats guide). Connected-account provisioning does not grant BloxClips staff any Whop account role.
Target frontend architecture
/dashboard/support is a shared outer dashboard destination. Clippers load their own feed. Authorized staff see the BloxClips-owned inbox list and open the same official ChatElement. The admin-only support route redirects to the shared route and is absent from admin tab navigation.
Target backend architecture
supportChat.ts is the sole support API mount. It has only channel resolution, token minting, and staff inbox/selection endpoints. There are no custom message endpoints, event streams, polling routes, message persistence, or legacy fallback APIs.
Target database state
The database contains WhopIdentity only for the BloxClips-to-Whop identity relationship. SupportConversation and SupportMessage are absent. No Whop feed or message is persisted locally.
Navigation architecture
dashboardNavItems contains Support at /dashboard/support for every dashboard user. The screen branches by existing BloxClips staff authorization returned by the dashboard auth context.
Environment variables
| Variable | Repository | Server-only | Purpose |
|---|---|---|---|
WHOP_API_KEY | Bloxclips-backend | Yes | Whop API key; needs company:create, company:basic:read, support_chat:create, support_chat:read, and scopes sufficient to mint support tokens. Obtain it from Whop Developer Dashboard. |
WHOP_COMPANY_ID | Bloxclips-backend | Yes | BloxClips Whop account ID in biz_... form, from the Whop dashboard URL/settings. It is not WHOP_COMPANY_ROUTE. |
WHOP_COMPANY_ROUTE | Bloxclips-backend | Yes | Existing public route slug for unrelated Whop lookups; not used as the chat company ID. |
Security invariants
- API key is never exposed to frontend code.
- Token users come only from authenticated server state.
- Customer channel creation always targets the caller's permanent identity.
- Staff inbox and selection require BloxClips RBAC and server-side BloxClips-company validation.
- Automatic provisioning uses only authenticated profile data and a one-to-one database mapping.
- Whop native support/team permissions enforce the embedded feed itself.
Historical implementation phases (completed)
- Audit code, database, auth, navigation, installed types, and official Whop docs.
- Finalize one permanent identity model and automatic provisioning.
- Use account-scoped support tokens plus Support Channels for customer and staff flows.
- Move Support to shared navigation and render all feeds with ChatElement.
- Delete custom/migration support infrastructure and update documentation.
Historical verification plan
- Run Prisma generation and validation plus backend TypeScript build and focused Node tests.
- Run frontend lint and production build.
- With valid Whop configuration, exercise a new clipper, a second clipper, and a staff account; verify automatic provisioning, stable per-clipper feeds, staff inbox/replies, reload behavior, and all authorization failures.
Files changed
Bloxclips-backend/src/lib/whopSupportChat.tsBloxclips-backend/src/api/routes/supportChat.tsBloxclips-backend/src/lib/whopSupportChat.test.tsBloxclips-backend/prisma/schema.prismaBloxclips-backend/prisma/migrations/20260831090000_finalize_whop_identity_model/migration.sqlBloxClips-frontend/features/support/api/supportChat.tsBloxClips-frontend/surfaces/content-rewards/screens/support/SupportScreen.tsx- Whop/support documentation.
Files deleted
Bloxclips-backend/scripts/migrateWhopChatIdentities.tsBloxclips-backend/prisma/migrations/20260505000000_add_support_chat/migration.sqlBloxclips-backend/prisma/migrations/20260830000000_add_whop_chat_identities/migration.sqlBloxclips-backend/prisma/migrations/20260830150000_remove_legacy_support_chat/migration.sql
Decisions made
WhopIdentity is the permanent mapping because it cleanly separates BloxClips-owned connected-account identity from Google/Discord provider credentials. The clean development migration creates only this model. Staff identities are automatically provisioned like all other users; staff authorization and Whop account team membership remain separate concerns.
Current blockers
No source-code blocker remains. Runtime verification requires a valid Whop API key with the documented permissions, the configured biz_ account, Whop team support access for a test staff identity, and authenticated BloxClips test sessions.
Final handoff
Support has one implementation: Whop Support Channels, BloxClips-managed account-scoped Whop identities/tokens, and Whop ChatElement. There is no BloxClips message store, realtime transport, custom composer, migration script, or admin identity exception.