Sales Portal

Sales Portal #

Classification: Restricted (describes source code, infrastructure, and access control).

The Sales Portal is an internal-only admin app for the Sales and Customer Success teams. It proxies Clerk so staff can manage customer organizations without direct Clerk-dashboard access, and it records an audit trail of every action to Datadog.

  • Code: apps/sales-portal/
  • Stack: TanStack Start (React 19) + Vite + Nitro, shared @bonsai/ui.
  • Hosts: dev dev-sales-portal.internal.gotofu.com, prod sales-portal.internal.gotofu.com (internal ALB, VPN/office restricted).
  • Doppler: shares the bonsai project (like webapp/tofu-mcp); synced to the bonsai-secret k8s secret at runtime.

What it does #

Area Capability
Organizations List/search (by name, slug, or Clerk org id), create (auto-slug + invite owners inline), rename; whole row is clickable
Members Two tabs — Members and Invited (invitee, status, date); invites are Owner-only
Feature flags Toggle any flag, including canary (replaces hand-editing in the Clerk dashboard)
Trials Constrained: extend expiry by 1 week, max entities fixed at 2, max extractions/month +10 (10s cooldown, cap 100)
Waitlist List waitlisted users (read-only)
Impersonation Eye icon in the member list (needs sales_portal:impersonate): mints a Clerk actor token and opens a recorded webapp session
Recordings Needs sales_portal:watch_recording — replay impersonation sessions (contain customer PII)
Audit trail Everyone with access — global (/audit) and per-org (org detail → Audit tab), read back from Datadog

Feature flags and trials are stored in Clerk organization metadata (publicMetadata.features, publicMetadata.trial_*, privateMetadata.data_retention_period_in_days). bonsapi’s organization.updated webhook mirrors these into the database, so writes made here stay in sync.

Access control (Clerk RBAC) #

“Feature or permission?” — both. A Clerk custom permission key is composed of a Feature namespace plus an action: org:<feature>:<permission>. The portal uses:

  • Feature (namespace): sales_portal
  • Permissions (the source of truth for gating): org:sales_portal:access (see the portal at all), org:sales_portal:impersonate (the member-list eye icon), org:sales_portal:watch_recording (the Recordings screen).
  • Attach these permissions to whatever roles you like in the Clerk dashboard (e.g. a sales role with just access, a csm role with access + impersonate, an owner role with all three). The portal checks the permissions on the caller’s internal-org membership — not role names. The audit trail is visible to anyone with access. Users without access get a “No access” screen with a sign-out button.

Access is decided server-side by looking up the caller’s membership in the internal gotofu Clerk org (SALES_PORTAL_INTERNAL_CLERK_ORG_ID) and reading that membership’s permission list — not the JWT has() helper, because a staff user usually has a customer org active and custom permissions only appear in the token for the active org. See apps/sales-portal/src/shared/server/auth.ts.

Do not confuse the three “feature” concepts in this repo. BonsAI feature flags are config/features.yaml → org publicMetadata.features (what the Feature Flags tab toggles). Clerk RBAC Features are permission namespaces like sales_portal. Clerk Billing features are unused (Tofu bills via Stripe) — if Clerk Billing is ever enabled, permission checks only pass when the permission’s Feature is in the org’s active plan.

Granting a staff member access #

  1. In the Clerk dashboard, open the internal gotofu organization.
  2. Add the user as a member with role org:sales or org:csm.
  3. No code change or redeploy is needed — the portal reads the role live (60s cache).

Audit trail #

Every mutation writes a Datadog log tagged is_audit_log: true (matching the backend audit_info! convention in libs/rust/bonsai-utils). Find them with:

service:sales-portal is_audit_log:true

Each event carries audit_type (OrgCreate, OrgNameUpdate, OrgInvitationCreate, FeatureFlagUpdate, TrialUpdate, ImpersonationStart), the actor’s user id + email, the target org/user, and before/after snapshots for flag/trial changes. Source: apps/sales-portal/src/shared/server/audit.ts.

Impersonation & recording #

CSM users can impersonate a customer user from an org’s Impersonate tab. The portal mints a short-lived Clerk actor token and opens https://<WEBAPP_HOST>/sign-in?__clerk_ticket=<token> in a new tab; Clerk’s <SignIn> redeems the ticket.

The impersonated webapp session is screen-recorded with rrweb: the ImpersonationRecorder (apps/webapp/src/shared/components/impersonation-recorder.tsx) dynamically loads rrweb only for impersonated sessions and streams event chunks to POST /api/impersonation-recording, which uploads them to a Google Drive shared drive. CSMs replay sessions in the portal under Recordings.

Runbooks #

Doppler keys (bonsai project) #

The portal shares the bonsai Doppler project. CLERK_SECRET_KEY, DATADOG_API_KEY and WEBAPP_HOST already exist there; add the rest:

Key Status Notes
VITE_CLERK_PUBLISHABLE_KEY add Clerk publishable key (client). Inlined at build. Same value as NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.
CLERK_SECRET_KEY exists
SALES_PORTAL_INTERNAL_CLERK_ORG_ID add Clerk org id (org_…) of the internal gotofu org.
WEBAPP_HOST exists Bare webapp host, e.g. app.gotofu.com; the portal prepends the scheme.
DATADOG_API_KEY exists
GOOGLE_SALES_PORTAL_SERVICE_ACCOUNT_JSON_B64 add Base64 of the Drive service-account JSON (also used by the webapp recorder).
GDRIVE_SALES_PORTAL_RECORDINGS_FOLDER_ID add Root folder id in the recordings shared drive (also used by the webapp recorder).

The existing bonsai-doppler-external-secret already syncs every bonsai key into bonsai-secret (dataFrom regexp .*), so no new ExternalSecret is needed. CI build uses the shared DOPPLER_TOKEN / DOPPLER_CONFIG secrets (same as webapp).

Clerk dashboard setup (dev AND prod instances) #

  1. Roles & Permissions → Custom permissions → create Feature sales_portal, then permissions sales_portal:access and sales_portal:impersonate.
  2. Create roles org:sales (access only) and org:csm (access + impersonate).
  3. Assign staff in the internal gotofu org; note that org’s id → SALES_PORTAL_INTERNAL_CLERK_ORG_ID.
  4. Confirm the Enhanced B2B Authentication add-on covers the extra custom roles, and that Clerk Billing is not enabled.

Google Drive service account (one-time) #

  1. In Google Cloud Console, create/reuse a project (e.g. tofu-sales-portal).
  2. Enable the Google Drive API.
  3. Create a service account sales-portal-recorder; add a JSON key; base64-encode it into GOOGLE_SALES_PORTAL_SERVICE_ACCOUNT_JSON_B64.
  4. Create a Shared Drive “Impersonation Recordings”. It must be a Shared Drive — service accounts have no personal Drive quota, so uploads to a normal folder fail with storageQuotaExceeded.
  5. Add the service-account email as Content manager on the Shared Drive; keep human membership restricted to the sales/CSM group (recordings contain customer data).
  6. Put the target folder id in GDRIVE_SALES_PORTAL_RECORDINGS_FOLDER_ID.
  7. All Drive API calls pass supportsAllDrives: true (+ includeItemsFromAllDrives: true on lists), or Shared Drive items are invisible.

Retention: recordings are not auto-deleted yet. Decide and implement a retention policy (manual cleanup or a scheduled job) before this sees heavy use.

Local development #

mise run sales-portal-dev   # Doppler sales-portal/dev + vite dev on :3040