Internal tool for the Testsigma sales and GTM team. Deployed on Vercel at battlecard-nine.vercel.app.
- Architecture Overview
- Environment Variables
- Firebase Setup & Auth
- User Management
- Slack Integration
- Vercel Blob Storage
- API Routes
- Cron Jobs
- Deploying Changes
- Common Issues & Fixes
Browser (React SPA)
↓ Firebase Google Auth (login)
↓ /api/* (Vercel serverless functions)
├── Claude (Anthropic) — AI answers + web search
├── Gong — call history & transcripts
├── Attive — AI analyst for your entire revenue stack (reads CRM, calls, and emails via Slack bot)
├── Custify — customer health, usage, renewals
├── HubSpot — CRM data
├── Salesforce — CRM data
├── G2 — buyer intent signals
├── Vercel Blob — KB, user groups, analytics cache
└── Slack — digest, feedback, Attive pings, access requests
Deployment: Push to main → Vercel auto-deploys (typically 2–3 min).
Repo: narain-testsigma/battlecard (private)
Vercel project: battlecard-nine
Set all of these in Vercel → Project Settings → Environment Variables. Never commit secrets to the repo.
| Variable | Description | Where to get it |
|---|---|---|
ANTHROPIC_KEY |
Claude API key | console.anthropic.com → API Keys |
GONG_ACCESS_KEY |
Gong API access key | Gong → Settings → API |
GONG_SECRET |
Gong API secret | Gong → Settings → API |
SLACK_BOT_TOKEN |
Slack bot token (xoxb-...) |
api.slack.com → Your App → OAuth Tokens |
SLACK_SIGNING_SECRET |
Validates incoming Slack events | api.slack.com → Your App → Basic Info |
ATTIVE_API_KEY |
Attive KB API key | Attive dashboard |
BCBLOB_READ_WRITE_TOKEN |
Vercel Blob storage | Vercel → Storage → your Blob store |
INTERNAL_API_TOKEN |
Internal auth between frontend ↔ API | Generate any random string (32+ chars) |
HUBSPOT_TOKEN |
HubSpot private app token | HubSpot → Settings → Integrations → Private Apps |
CUSTIFY_API_KEY |
Custify API key | Custify dashboard → API |
CUSTIFY_API_URL |
Custify endpoint | https://us.api.custify.com |
G2_API_TOKEN |
G2 intent data | G2 Buyer Intent dashboard |
FACTORS_API_KEY |
Factors.ai intent data | Factors dashboard |
RESEND_API_KEY |
Transactional email | resend.com → API Keys |
NOTIFY_EMAIL |
Where feedback emails go | e.g. narain@testsigma.com |
GAMMA_API_KEY |
Gamma deck generation | gamma.app |
GITHUB_TOKEN |
Posts feedback as GitHub issues | GitHub → Settings → PAT (repo scope) |
GITHUB_REPO |
Target repo for feedback issues | e.g. narain-testsigma/sdr-navigator |
SHEET_WEBHOOK_URL |
Google Sheets webhook for analytics | Google Apps Script deploy URL |
SHEET_SECRET |
Shared secret for webhook | Set same value in Sheets script |
SITE_PASSWORD |
Legacy password auth (fallback) | Set any strong password |
ADMIN_PASSWORD |
Legacy admin password auth | Set any strong password |
APP_URL |
Public app URL | https://battlecard-nine.vercel.app |
| Variable | Description |
|---|---|
REACT_APP_FIREBASE_API_KEY |
Firebase project API key |
REACT_APP_FIREBASE_AUTH_DOMAIN |
e.g. your-project.firebaseapp.com |
REACT_APP_FIREBASE_PROJECT_ID |
Firebase project ID |
REACT_APP_FIREBASE_APP_ID |
Firebase app ID |
REACT_APP_INTERNAL_API_TOKEN |
Same value as INTERNAL_API_TOKEN above |
Note:
REACT_APP_*variables are baked into the frontend bundle at build time. Changing them requires a redeploy.
The app uses Firebase Google OAuth — users sign in with their Google account, no passwords.
Only @testsigma.com and @speargrowth.com emails can log in. Enforced in two places:
src/Home.jsx—allowedDomainsarray (frontend check after OAuth)- Firebase Console → Authentication → Authorized domains
- Go to console.firebase.google.com
- Select the project (same as
REACT_APP_FIREBASE_PROJECT_ID) - Authentication → Sign-in method → Google → Enable
- Authentication → Authorized domains → Add:
battlecard-nine.vercel.applocalhost(for local dev)- Any preview deployment domains if needed
- Project Settings → General → Your apps → get the config values for env vars
Two code changes needed:
src/Home.jsx→allowedDomainsarrayapi/user-groups.js→allowedDomainsarray (controls who can be added via the admin UI)
Then redeploy.
Users who sign in but have no tier assigned see a "Request Access" banner with a button. Clicking it posts their name + email to #battlecard-deploy-feedback in Slack so an admin can grant access.
| Tier | Access |
|---|---|
sales |
Cold Call, Competition Intel, Content Review, Demo Prep, AMA, Learn |
csm |
Everything in sales + Customer Health |
admin |
Everything including Analytics, User Access, Knowledge Base, Hot Accounts, Account 360, Hall of Fame |
spear |
AMA only (for Spear Growth agency users) |
marketing |
Marketing OS and all marketing apps (Ad Copy, Blog Writer, Campaign Planner, etc.) |
manager |
Coaching Dashboard |
ceo_office |
Feedback Loop — admin bypass is disabled; must be explicitly assigned |
@testsigma.com accounts automatically get sales tier on first login.
@speargrowth.com accounts need to be manually assigned spear tier.
- Go to the app → Admin section → User Access
- Enter their email (must be
@testsigma.comor@speargrowth.com) - Select their tier(s)
- Click Add
Changes take effect on their next sign-in.
Same page — click × next to their name.
Used for testing tier visibility without logging out:
- Go to User Access
- Click 👁 Spoof next to any user
- App reloads showing you exactly what that user sees
- Yellow banner at top confirms who you're spoofing
- Click Exit spoof to return to your admin view
Spoof is session-only — closes when the tab closes.
User → tier mappings are stored in Vercel Blob at config/user-groups.json.
Managed entirely through the User Access UI — no manual blob edits needed.
- App name: Testsigma Intelligence (or similar)
- Workspace: Testsigma Slack
- Needs these OAuth scopes:
chat:write— post messageschat:write.public— post to channels without joiningchannels:history+conversations.replies— read thread repliesfiles:read— read Attive's PDF responseschannels:read— list channelsusers:read— resolve user names
| ID | What it is |
|---|---|
C0AB3QJ2VQW |
#attive-pmm — AMA pings Attive bot here |
C098WC88ABD |
#ai-generated-analysis — KB Intel monitors this |
C0987FW4154 |
#pmm-and-product — KB Intel monitors this |
C0ARCJ3SBA7 |
#battlecard-deploy-feedback — Request Access pings + negative feedback alerts |
U09FP9X2M53 |
Attive bot user ID (the bot that replies with account briefs) |
If any of these change (channel renamed, Attive bot reinstalled), update the hardcoded IDs in the relevant api files.
Attive is the AI analyst for your entire revenue stack. It reads your CRM, calls, and emails together — so you can ask why deals are lost, which accounts are at risk, and what your pipeline actually looks like.
When a user asks AMA about an account:
- AMA posts
@Attive <AccountName> — <question>in#attive-pmm api/attive-poll.jspolls the thread every 3s for up to 55s- Attive replies (text or PDF) — text is extracted and passed to Claude
- Claude answers with Attive data as primary source, supplemented by Gong call context
Attive stores accounts by compact name (no spaces): "State Farm" → ping as "Statefarm".
Adding a 📌 (pushpin) reaction to any Attive message in Slack saves it to the Knowledge Base:
- Text messages → saved directly to Google Sheet
- PDF attachments → Slack DMs the reactor asking them to use
/kb-addto paste the content
All persistent data lives in Vercel Blob (token: BCBLOB_READ_WRITE_TOKEN).
| Blob path | Contents | Updated by |
|---|---|---|
kb/claude-core.md |
Main shared knowledge base | KB Admin UI or api/kb-admin.js |
kb/claude-intel.md |
Weekly Slack intel summary | api/kb-intel.js (cron) |
config/user-groups.json |
User → tier mappings | User Access UI |
accounts/*.json |
Cached account intelligence | api/account-360.js, api/demo-prep.js |
analytics/logs.json |
Usage analytics log | api/analytics.js |
hall-of-fame/*.json |
Hall of Fame entries and reactions | api/hall-of-fame.js |
To view/edit blobs directly: Vercel dashboard → Storage → your Blob store.
| Endpoint | What it does |
|---|---|
POST /api/ama |
Account context fetch (Attive + Gong) or Claude call with web search. Body: { accountName, question, messages } |
POST /api/attive-poll |
Polls #attive-pmm thread for Attive's reply |
GET /api/gong |
Fetches and caches Gong calls for an account |
GET /api/account-360 |
Full account intel (HubSpot + Gong + G2 + Attive) |
POST /api/demo-prep |
Demo prep brief generation |
POST /api/cold-call |
Live call intelligence |
POST /api/content-review |
Content review against brand standards |
GET /api/custify |
Custify customer health data — all active companies normalised with health scores, ARR, renewal dates, usage metrics |
POST /api/job-signals |
Scans a company's active QA job listings for competing tool expertise (Provar, Tosca, Selenium, Playwright, etc.) using Claude web search |
GET /api/g2 |
G2 buyer intent signals by domain |
GET /api/kb |
Fetches shared KB (claude-core + claude-intel) |
POST /api/kb-admin |
Update claude-core.md knowledge base |
POST /api/kb-intel |
Run Slack intel capture + summarise |
GET /api/user-groups |
List all user → tier mappings, or ?email=x to get tiers for a specific user |
POST /api/user-groups |
Add/update a user's tiers |
DELETE /api/user-groups |
Remove a user |
POST /api/analytics |
Log a usage event |
POST /api/slack |
Slack event handler (signing secret validated) — handles reaction_added (📌), /kb-add slash command, modal submissions, and internal post_message calls |
GET /api/slack-digest |
Triggered by cron — sends daily hot accounts digest to Slack |
POST /api/feedback |
Field feedback → email PMM + GitHub issue |
GET /api/gong-calls |
Recent calls list for Hall of Fame |
GET /api/hall-of-fame |
Hall of Fame entries |
POST /api/hall-of-fame |
Add Hall of Fame entry |
GET /api/gamma |
Generate Gamma deck |
POST /api/narrative-pptx |
Generate PowerPoint narrative |
POST /api/auth |
Legacy password auth |
POST /api/event-prep |
Event prep brief generation. Shareable via public /event-prep/:eventId route |
GET /api/history |
Query usage history log |
GET /api/gong-scorecards |
Gong scorecard data |
GET /api/g2-ratings |
G2 ratings data |
GET /api/attive-pdf |
Fetch and parse Attive PDF attachments from Slack |
POST /api/onboarding |
Onboarding flow handler |
GET /api/salesforce |
Salesforce CRM data |
Most API routes require the x-internal-token header matching INTERNAL_API_TOKEN. Exceptions: /api/auth, /api/slack (uses Slack signing secret instead).
Defined in vercel.json:
| Schedule | Route | What it does |
|---|---|---|
0 4 * * 1-5 |
/api/slack-digest |
Weekdays 4 AM UTC — sends hot accounts digest to Slack |
To add a cron: edit vercel.json → push to main.
Cron logs are visible in Vercel → Functions → Logs.
git add .
git commit -m "describe change"
git push origin main
# Vercel auto-deploys in ~2 minWatch the build at: vercel.com → battlecard-nine → Deployments
If the build fails: Check the build log for ESLint errors — Vercel treats warnings as errors (CI=true). Common culprits: unused variables, missing dependencies.
- Check their email domain is in
allowedDomainsinsrc/Home.jsx - Check Firebase Console → Authorized domains includes the app URL
- Check they've been added to User Access with the right tier
- If they see a "Request Access" button, they are authenticated but have no tier — add them via User Access
- Attive stores accounts without spaces: try
"Statefarm"not"State Farm" - The ping includes the question now — if still failing, check
SLACK_BOT_TOKENis set and the Attive bot (U09FP9X2M53) is in#attive-pmm
- Check Vercel function logs for the
gong.jsextraction step - Gong uses a 1-hour module-level cache — a cold start clears it
- Verify
GONG_ACCESS_KEYandGONG_SECRETare set
api/attive-poll.jspolls for 55s — Attive can take up to 60s for large accounts- Check Vercel logs for
Attive poll: got reply after Xs - If Attive bot is down or slow, AMA still answers using Gong data only
- Check
CUSTIFY_API_KEYis set in Vercel environment variables - Check Vercel logs for the
/api/custifyfunction — will show the raw count vs filtered count - The endpoint filters out churned accounts —
rawin the response shows the unfiltered count
- Health score fields use Salesforce
__csuffix — e.g.ghs__c,risk_rating__c risk_rating__cis a string ("Low"/"Medium"/"High"/"Critical"), not a number- If fields are empty for all accounts, check Custify custom attribute mappings
- Remove the unused variable — Vercel treats ESLint warnings as errors in CI
- Go to Admin → Knowledge Base and check the blob editor
- Or call
POST /api/kb-adminwith the updated content claude-intel.mdis auto-updated by the weekday cron
- Spoof is stored in
sessionStorage— must be same tab - Only works if you are an admin — non-admins can't activate spoof
- Check
SLACK_BOT_TOKENis set and haschat:write.publicscope - Channel ID
C0ARCJ3SBA7is hardcoded insrc/Home.jsx— verify it matches#battlecard-deploy-feedback
- Requires
ANTHROPIC_KEYandINTERNAL_API_TOKENset in Vercel - Uses Claude web search — max duration is 45s per company, 20 companies scanned in parallel
- Results are cached in sessionStorage — refresh the page to clear the cache and re-scan
| Role | Person |
|---|---|
| Product / Owner | Narain (narain@testsigma.com) |
| Vercel / GitHub access | Narain |
| Firebase project | Narain |
| Attive bot | Attive team |