Stratify
Legal and compliance

B2B Partner Onboarding & Key Provisioning Procedure

docs/compliance/isms/procedures/PRO-03-partner-b2b-onboarding.md

Source updated 03. Aug. 2026

B2B Partner Onboarding & Key Provisioning Procedure

What already exists vs. what this procedure adds

The technical self-serve flow is real and running: a partner applicant can sign in and provision a sandbox partner + API key today at /partners/sandbox (apps/web/app/(partner)/partners/sandbox/actions.ts). What does not exist yet is a formal onboarding checklist that governs when a sandbox partner is allowed to become a production (live) partner, beyond the technical KYB-checklist gate already wired into the code. This procedure is net-new — it documents the process this ISMS expects around the existing code path, not something that has already been run end to end as a documented procedure.

Roles

  • Applicant — an employee of a prospective B2B partner (a neobroker), self-serves through

/partners/sandbox.

  • ISMS Owner (Toby) — reviews and actions KYB items, promotes partner status, issues/revokes

production keys. Today the only person who holds admin/founder role and can call these actions (apps/web/app/(admin)/partners/actions.ts:14-22, requireAdmin()).

  • Compliance actor — completes KYB checklist items (requireComplianceActor(),

apps/web/app/(admin)/partners/actions.ts:71-114). Today this is also Toby in practice, since no one else holds the compliance role — stated honestly, same as GOV-03's RACI notes.

1. Application intake (sandbox)

  1. The applicant signs in and submits legal name + applicant name via /partners/sandbox

(requestSandbox(), apps/web/app/(partner)/partners/sandbox/actions.ts:18-87).

  1. The system creates: a partners row (status: 'sandbox'), a partner_users row binding the

applicant to that partner, and mints a sandbox API key scoped ['signals:read', 'audit:read'] (generateApiKey(), hash stored, plaintext returned once).

  1. The provisioning event is recorded to audit_log as partner.sandbox.provisioned, and a

welcome email is sent (sendSandboxWelcomeEmail).

  1. Idempotency: a user who already has a partner_users row cannot re-provision through this

path (error: 'already_provisioned') — they use /partners/console rotate instead. This is an existing technical safeguard, not a manual check this procedure needs to add.

  1. At this point the partner has sandbox-only access — no production subscriber data flows to

a sandbox key. This is the correct default: the self-serve path itself does not grant production access, only a promotion to live (step 3 below) does.

2. KYB-equivalent check (Phase A gate)

Stratify does not run a formal KYB (Know Your Business) process today in the sense a regulated neobroker would — no external verification vendor, no document upload flow. What exists is an internal checklist that must be fully completed before a partner can be promoted to live (apps/web/lib/admin/partner-kyb.ts, PARTNER_KYB_ITEMS). Phase A items required before go-live:

ItemLabel
legal-entityLegal entity verified
contractB2B agreement signed
webhook-testedWebhook endpoint tested (sandbox)
compliance-reviewCompliance review complete
security-reviewSecurity / API usage review

(A sixth item, production-sla, is Phase-C-scoped and not required for the Phase A go-live gate today — PARTNER_PHASE_A_ITEMS filters it out.)

Steps:

  1. The compliance actor works through each Phase A item with the applicant/partner: confirming

legal-entity identity (company registration lookup or equivalent — no vendor integration exists, this is a manual check today), obtaining a signed B2B agreement, testing the partner's declared webhook endpoint against the sandbox key, and completing an internal compliance and security review of the partner's intended use (what data they'll pull, what their own downstream use of Stratify signals is).

  1. Each item is marked complete via setPartnerKybCheck()

(apps/web/app/(admin)/partners/actions.ts:71-114), which requires requireComplianceActor(), records completed_by and notes, and writes partner.kyb.completed to audit_log.

  1. Gate enforcement is code-level, not just procedural: setPartnerStatus() refuses to move a

partner to live unless getPartnerPhaseAKybComplete() returns true — i.e. all five Phase A items are marked complete (apps/web/app/(admin)/partners/actions.ts:38-42, canPartnerGoLive() in apps/web/lib/admin/go-live-gates.ts). A partner cannot be promoted by skipping this checklist; the application itself blocks it.

3. Go-live promotion

  1. Once all Phase A KYB items are complete, the ISMS Owner (holding admin or founder role)

calls setPartnerStatus({ partnerId, status: 'live' }) (apps/web/app/(admin)/partners/actions.ts:29-62).

  1. This is recorded to audit_log as partner.status.changed.
  2. The partner's existing sandbox key is not automatically upgraded in scope or revoked

review whether a fresh, production-scoped key should be issued at this point (step 4 below) rather than assuming the sandbox key silently becomes a production credential.

4. Production key issuance

  1. The ISMS Owner issues a key via rotatePartnerKey()

(apps/web/app/(admin)/partners/actions.ts:154-183), which inserts a new partner_api_keys row scoped ['signals:read', 'audit:read'] and returns the plaintext key exactly once ({ ok: true, apiKey: key.full }) — it is not retrievable again after this call.

  1. Recorded to audit_log as partner.key.issued.
  2. Hand the key to the partner over a channel that isn't persisted in plaintext (the same

constraint as PRO-01's secrets handling) — the welcome-email flow for the sandbox key is a convenience for a low-stakes sandbox credential; a production key should be transmitted more carefully.

  1. Scopes today are fixed (signals:read, audit:read — every issuance uses the same array).

There is no per-partner scope customization yet; note this as a current limitation rather than implying finer-grained scoping exists.

5. Key revocation (offboarding or suspected compromise)

  1. The ISMS Owner calls revokePartnerKey({ partnerId, keyId })

(apps/web/app/(admin)/partners/actions.ts:121-150), which sets revoked_at on the specific key row (is('revoked_at', null) guards against double-revocation).

  1. Recorded to audit_log as partner.key.revoked.
  2. Suspected compromise: revoke the compromised key immediately, then issue a replacement via

rotatePartnerKey() (step 4 above) rather than reusing scope assumptions from the old key. docs/secrets.md's rotation table already names this trigger: "Partner-key suspected compromised → Revoke in partner_api_keys, issue replacement via /admin/partners."

  1. Full offboarding: set partners.status to suspended via setPartnerStatus() (recorded

as partner.status.changed) in addition to revoking all active keys for that partner — a suspended partner should have zero active keys, not just a changed status flag.

Evidence

Every step above already writes to audit_log (a real, append-only, chain-hashed record — register/facts.md). That is the primary evidence trail for this procedure: partner.sandbox. provisioned, partner.kyb.completed/partner.kyb.reopened, partner.status.changed, partner.key.issued, partner.key.revoked. No separate provisioning/revocation log needs to be maintained by hand — querying audit_log filtered to subject_type = 'partner' for a given subject_id reconstructs the full onboarding history for that partner.

Gaps this procedure does not close

  • No external KYB/legal-entity verification vendor is integrated — the legal-entity check is a

manual judgment call today, not a verified lookup.

  • No per-partner scope customization exists — every key gets the same two scopes.
  • The compliance-review and security-review checklist items have no defined internal standard

they're checked against yet (no rubric, just a checkbox) — a future revision of this procedure should define what "complete" means for each.

Review

Reviewed whenever the KYB item list (PARTNER_KYB_ITEMS) changes in code, or annually otherwise.