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, prodsales-portal.internal.gotofu.com(internal ALB, VPN/office restricted). - Doppler: shares the
bonsaiproject (like webapp/tofu-mcp); synced to thebonsai-secretk8s 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
salesrole with just access, acsmrole 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 withaccess. Users withoutaccessget 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→ orgpublicMetadata.features(what the Feature Flags tab toggles). Clerk RBAC Features are permission namespaces likesales_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 #
- In the Clerk dashboard, open the internal gotofu organization.
- Add the user as a member with role
org:salesororg:csm. - 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) #
- Roles & Permissions → Custom permissions → create Feature
sales_portal, then permissionssales_portal:accessandsales_portal:impersonate. - Create roles
org:sales(access only) andorg:csm(access + impersonate). - Assign staff in the internal gotofu org; note that org’s id →
SALES_PORTAL_INTERNAL_CLERK_ORG_ID. - 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) #
- In Google Cloud Console, create/reuse a project (e.g.
tofu-sales-portal). - Enable the Google Drive API.
- Create a service account
sales-portal-recorder; add a JSON key; base64-encode it intoGOOGLE_SALES_PORTAL_SERVICE_ACCOUNT_JSON_B64. - 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. - 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).
- Put the target folder id in
GDRIVE_SALES_PORTAL_RECORDINGS_FOLDER_ID. - All Drive API calls pass
supportsAllDrives: true(+includeItemsFromAllDrives: trueon 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