Slack Revenue Alerts — Data Flow & SOC 2 Posture
Modules:Silvia-Web/lib/slack-revenue.ts— per-event alerts fired from the Stripe webhook handlerSilvia-Admin/lib/slack-digest.ts— recurring 14:05 UTC daily pulse + Monday 15:00 UTC weekly digest
Why the digest lives inSubprocessor: Slack (data sent via Incoming Webhook to a private channel) Channel:Silvia-Admin:Silvia-Webis at its Vercel cron-slot ceiling. Both repos run against the same Supabase project, so the digest crons access the samestripe_subscriptions/stripe_subscription_history/all_users/ai_token_usage_dailytables and the sameget_user_token_usage_in_window/get_cap_crossings_in_rangeRPCs that the per-event alerts use — the two surfaces remain in numerical agreement by construction. TheSilvia-Admindigest module sends the same data classes to the same Slack channel under the same SOC 2 control SLACK-REV-001; nothing about the data-minimization posture changes from this split.
#silvia-revenue (same channel for both surfaces; distinct visual treatment)
Audience: Named ops members only
This document describes the data we send to Slack from the revenue-alerts
modules, why we send it, and the SOC 2 Type 2 controls in place.
What we send
Every alert contains exactly these classes of data:What we deliberately do NOT send
SOC 2 Type 2 control description
Control name: SLACK-REV-001 — Data minimization for revenue notifications Stated control: The Slack revenue notifications module sends only pseudonymous internal identifiers and aggregate business metrics to the configured Slack webhook. Direct PII (names, emails, phone numbers) and externally-resolvable identifiers (Stripe customer IDs) are excluded by design and verified at code-review time. Operating evidence:-
Code review:
lib/slack-revenue.tsimports a fixed allowlist of non-PII helpers from./token-limit(priceIdToTier,tierToPriceId,calendarMonthBounds) and./token-limit-client(TIER_LIMITS,TIER_NAMES,TIER_PRICES_USD,formatTokens, and theTiertype). It does NOT importdecryptFirstName,decryptEmail, or any other decryption function. The rule the reviewer enforces is “no module that returns plaintext from an encrypted PII column may be imported here” — adding any new helper that does NOT match that rule is the only thing requiring this enumeration to update. Pull request review enforces this. -
Database column allowlist: The
getUserContexthelper selects onlycreated_atfromall_users. It does not selectfirst_name,email,phone_number, or any other PII column. It also reads non-PII subscription metadata (created_at/stripe_subscription_idfromstripe_subscription_history;price_id/created_atfromstripe_subscriptions) to derive prior-paid tenure and tier for the alert’s “Lifecycle” field — timestamps and a Stripe subscription / price identifier, none of which are PII. -
Per-event field allowlist: The
buildEventFieldsswitch emits only Clerk ID, tier, price, dates, and aggregate counters. It does not reference Stripe customer ID from the event payload. -
Dedup table audit: The
slack_revenue_alerts_senttable is the only persistent record of “what alerts fired and when.” Queryable for audit purposes. -
Channel access control:
SLACK_REVENUE_WEBHOOK_URLpoints at a private Slack channel with named, restricted membership. Slack itself is covered by our Data Processing Agreement and listed as a subprocessor in the privacy policy.
Escalation path
If the SOC 2 auditor determines that even the Clerk ID is too identifying to send to a third-party SaaS, the next step is:- HMAC-SHA256 the Clerk ID with a server-side secret key (HMAC takes a confidential cryptographic key per NIST FIPS 198-1 / RFC 2104; the key must be kept secret, unlike a salt which is a non-secret password-hashing primitive)
- Truncate to 8-12 hex chars
- Post the truncated hash instead of the raw Clerk ID
- Build an admin tool that accepts a hash and returns the Clerk ID (or the user record) so ops can still act on alerts