This is a Next.js ARR dashboard.
/HubSpot ARR report/stripeStripe ARR report/stripe-arr-correctStripe ARR (Correct) report/combined-all-subsCombined ARR by customer (HubSpot cloud C-ARR + Stripe through-MRR merge)/commissionsAdmin-only owner commission and Stripe clawback report/scorecardsGoogle-authenticated team performance dashboards/tv/scorecards/[team]Read-only, HTTP Basic Auth performance dashboard for Yodeck/tofuMonthly TOFU ARR bridge (Beginning/New/Expansion/Contraction/Churn/Ending)/quickbooksQuickBooks OAuth + data access pagePOST /api/reportHubSpot report APIGET|POST /api/stripe-reportStripe report APIGET /api/stripe-report/exportStripe full CSV export API (all filtered/grouped rows)GET|POST /api/stripe-arr-correct-reportStripe ARR (Correct) API (BigQuery profile)GET /api/stripe-arr-correct-report/exportStripe ARR (Correct) CSV export APIGET|POST /api/combined-all-subs-reportCombined HubSpot+Stripe customer ARR APIPOST /api/commissionsAdmin-only monthly commissions APIGET /api/tv/team-scorecardsRead-only, HTTP Basic Auth scorecard API for YodeckGET|POST /api/tofu-reportMonthly TOFU ARR bridge API based on Combined All SubsGET|POST /api/hubspot-current-metrics-syncPush current ARR/contracted ARR values into HubSpot deal propertiesGET|POST /api/stripe-syncStripe sync APIGET /api/stripe-sync/statusStripe sync health/status APIGET|POST /api/stripe-bigquery-refreshRebuild Stripe external BigQuery tables from GCS bucket foldersGET|POST /api/stripe-upcoming-syncTrigger Cloud Run upcoming-invoice snapshot sync jobGET|POST /api/stripe-upcoming-snapshots-cleanupDelete older same-day upcoming-invoice snapshots in BigQuery (keep latest snapshot for the day)GET /api/quickbooks/connectStart Intuit OAuthGET /api/quickbooks/callbackIntuit OAuth callbackGET /api/quickbooks/statusQuickBooks connection statusGET /api/quickbooks/keepaliveRefresh QuickBooks tokens (for cron keepalive)GET /api/quickbooks/company-infoFetch QuickBooks CompanyInfoPOST /api/quickbooks/queryRun a QuickBooks SQL-like queryPOST /api/quickbooks/disconnectClear saved QuickBooks tokensGET|POST /api/slack/daily-arr-summarySend daily Projected ARR metrics to Slack (DM/channel)GET|POST /api/billing/monthly-draft-invoicesCreate review-only Stripe draft invoices from Closed Won sales-led HubSpot deals
The /tv/scorecards directory and every /tv/scorecards/[team] page use HTTP Basic Authentication instead of Google sign-in. Configure both variables in Vercel:
TV_DASHBOARD_USERNAMETV_DASHBOARD_PASSWORD
Use a dedicated, randomly generated password containing only URL-safe ASCII characters. Add a Yodeck Web Page using this format, percent-encoding the credentials when necessary:
https://TV_USERNAME:TV_PASSWORD@YOUR_DOMAIN/tv/scorecards/sales
Available team slugs are engineering, product, sales, account-management, delivery, support, marketing, finance, and people-ops. After Basic Auth succeeds, the app exchanges it for a signed, HTTP-only TV session cookie and removes the credentials from the visible browser URL. Both the page and the separate GET-only scorecard API remain authenticated. TV pages disable management controls, prevent search indexing and browser caching, and refresh calculated data every five minutes.
Vercel cron runs Stripe sync automatically every 5 minutes:
*/5 * * * *(/api/stripe-sync)0 1-23/2 * * *(/api/stripe-bigquery-refresh) refresh of bucket-backed external Stripe tables every 2 hours10 */2 * * *(/api/stripe-upcoming-sync) trigger upcoming invoice snapshot Cloud Run job every 2 hours55 23 * * *(/api/stripe-upcoming-snapshots-cleanup) end-of-day cleanup: keep only latest same-day upcoming snapshot5 * * * *(/api/hubspot-current-metrics-sync) hourly HubSpot deal metric property update0 */6 * * *(/api/quickbooks/keepalive) QuickBooks OAuth token keepalive5 9 1 * *(/api/billing/monthly-draft-invoices) monthly Stripe draft-invoice generation at 09:05 UTC on the first day
/api/stripe-sync accepts:
{
"startDate": "2025-01-01",
"endDate": "2026-01-31",
"force": false,
"iterations": 12,
"reset": false
}reset: true clears existing sync state before backfilling again.
Use /api/stripe-sync/status to verify live progress. It returns:
stats.storage(vercel_bloborlocal_tmp)stats.itemCountstats.rangeExhaustedsecondsSinceUpdatehealthy
/api/stripe-bigquery-refresh accepts:
{
"dryRun": false
}The route is cron-authenticated with CRON_SECRET (same pattern as /api/stripe-sync) and can also be triggered manually.
/api/stripe-upcoming-snapshots-cleanup accepts:
{
"dryRun": false,
"targetDate": "2026-03-24",
"profile": "stripe_arr_correct"
}Notes:
targetDateis optional (YYYY-MM-DD, UTC). If omitted, current UTC date is used.profilesupportsstripe_arr_correct(default) ordefault.- The cleanup keeps the latest snapshot key for the target day and deletes other rows for that same day.
/api/stripe-upcoming-sync accepts:
{
"projectId": "botpress-stripe-data-pipeline",
"region": "northamerica-northeast1",
"jobName": "stripe-upcoming-line-sync"
}Notes:
- All fields are optional; defaults come from env vars:
STRIPE_UPCOMING_SYNC_PROJECT_IDSTRIPE_UPCOMING_SYNC_REGIONSTRIPE_UPCOMING_SYNC_JOB
- Route is cron-authenticated with
CRON_SECRETand triggers Cloud Run Jobs API usingGOOGLE_SERVICE_ACCOUNT_JSONcredentials.
/api/hubspot-current-metrics-sync accepts:
{
"asOfDate": "2026-03-21",
"dryRun": false,
"contractedArrField": "contracted_arr",
"currentArrField": "current_arr",
"syncedAtField": "arr_sync_date"
}Notes:
contractedArrFielddefaults tocontracted_arr(orHUBSPOT_CONTRACTED_ARR_FIELD)currentArrFieldis optional (HUBSPOT_CURRENT_ARR_FIELD)syncedAtFieldis optional (HUBSPOT_ARR_SYNC_DATE_FIELD)- Uses the same
INCLUDED_DEALSTAGEfilter as HubSpot report calculations
/api/slack/daily-arr-summary accepts:
{
"dryRun": false
}Required env vars for Slack:
SLACK_BOT_TOKENSLACK_DAILY_USER_IDS(comma-separated DM targets; ignored ifSLACK_ARR_DAILY_CHANNEL_IDis set)
Optional:
SLACK_ARR_DAILY_CHANNEL_ID(post to a channel instead of DM)SLACK_HANY_USER_ID(legacy single DM fallback ifSLACK_DAILY_USER_IDSis not set)
Stripe sync state is persisted in Vercel Blob when configured.
Required for persistent shared storage:
ARRP_READ_WRITE_TOKENorAARP_READ_WRITE_TOKENorBLOB_READ_WRITE_TOKEN
Optional blob key path:
STRIPE_SYNC_BLOB_PATH(defaultarr/stripe-sync-store-v1.json)BLOB_STORE_ACCESS(privatedefault, setpubliconly if you intentionally use a public store)
If blob token is missing, local /tmp fallback is used (not persistent across redeploys/instances).
- In the Intuit app dashboard, set your redirect URI to:
https://YOUR_DOMAIN/api/quickbooks/callback
- In Vercel, configure:
QUICKBOOKS_CLIENT_IDQUICKBOOKS_CLIENT_SECRETQUICKBOOKS_REDIRECT_URI
- Optional:
QUICKBOOKS_ENV(productiondefault,sandboxsupported)QUICKBOOKS_SCOPES(defaultcom.intuit.quickbooks.accounting)QUICKBOOKS_MINOR_VERSION(default75)QUICKBOOKS_TOKEN_BLOB_PATH(defaultarr/quickbooks/tokens-v1.json)QUICKBOOKS_TOKEN_STORE_PATH(default/tmp/arr-quickbooks-tokens-v1.json)
Fallback env names are also accepted:
- client id:
INTUIT_CLIENT_ID,QB_CLIENT_ID,CLIENT_ID - client secret:
INTUIT_CLIENT_SECRET,QB_CLIENT_SECRET,CLIENT_SECRET - redirect URI:
INTUIT_REDIRECT_URI,QB_REDIRECT_URI,REDIRECT_URI
You can read Stripe line items directly from BigQuery without removing blob sync. Blob persistence code remains in place and can be switched back at any time.
Set:
STRIPE_DATA_SOURCE=bigqueryGOOGLE_SERVICE_ACCOUNT_JSON(orGOOGLE_SERVICE_ACCOUNT_JSON_BASE64)BIGQUERY_PROJECT_ID(optional if present in service account JSON)BIGQUERY_STRIPE_TABLE(full table id:project.dataset.table)BIGQUERY_LOCATION(optional, defaultUS)BIGQUERY_TS_UNIT(millisecondsdefault, orseconds)BIGQUERY_SCHEMA_MODE(int_tsdefault, usetimestampfor tables with TIMESTAMP columns likeperiod_start/period_end)BIGQUERY_STRIPE_SERVING_TABLE(optional but recommended for speed; full table id with standardized int timestamp columns)BIGQUERY_SERVING_TS_UNIT(millisecondsdefault, orseconds; used only withBIGQUERY_STRIPE_SERVING_TABLE)
To keep Blob as source (default), set:
STRIPE_DATA_SOURCE=blob
/stripe-arr-correct is pinned to BigQuery and uses profile-specific env vars when present.
Defaults are pinned to avoid inheriting old Stripe settings:
- table:
botpress-stripe-data-pipeline.stripe.invoice_lines_helper - schema mode:
int_ts - ts unit:
milliseconds - target currency:
USD - serving table: disabled unless
BIGQUERY_STRIPE_ARR_CORRECT_SERVING_TABLEis explicitly set
Set these for the corrected source:
BIGQUERY_STRIPE_ARR_CORRECT_PROJECT_IDBIGQUERY_STRIPE_ARR_CORRECT_LOCATION(optional, default fallbackBIGQUERY_LOCATIONthenUS)BIGQUERY_STRIPE_ARR_CORRECT_TABLE(full table id:project.dataset.table)BIGQUERY_STRIPE_ARR_CORRECT_SERVING_TABLE(optional)BIGQUERY_STRIPE_ARR_CORRECT_SCHEMA_MODE(optional; default fallbackBIGQUERY_SCHEMA_MODE)BIGQUERY_STRIPE_ARR_CORRECT_TS_UNIT(optional; default fallbackBIGQUERY_TS_UNIT)BIGQUERY_STRIPE_ARR_CORRECT_SERVING_SCHEMA_MODE(optional; default fallbackBIGQUERY_SERVING_SCHEMA_MODE)BIGQUERY_STRIPE_ARR_CORRECT_SERVING_TS_UNIT(optional; default fallbackBIGQUERY_SERVING_TS_UNIT)
Optional profile-specific credential and currency overrides:
GOOGLE_SERVICE_ACCOUNT_JSON_STRIPE_ARR_CORRECT(orGOOGLE_SERVICE_ACCOUNT_JSON_BASE64_STRIPE_ARR_CORRECT)STRIPE_ARR_CORRECT_TARGET_CURRENCYSTRIPE_ARR_CORRECT_DATA_SOURCE(bigqueryorblob; route currently forces BigQuery)
Expected BigQuery columns (or aliases in a view):
customer_id,invoice_idline_item_id,line_item_descriptionamount_minor,currency,quantityperiod_start_ts,period_end_ts,invoice_created_ts
If using raw TIMESTAMP columns (amount, id, description, period_start, period_end, invoice_id, currency, quantity), set:
BIGQUERY_SCHEMA_MODE=timestamp
For fastest performance and to avoid timeout on large ranges, use a serving table (BIGQUERY_STRIPE_SERVING_TABLE) that already contains the standardized columns above.
If your Stripe lake is in GCS (for example gs://YOUR_BUCKET/livemode/<folder>/...) and you want one BigQuery table per folder in project.stripe, run:
cd arr_calculator
GCS_BUCKET=YOUR_BUCKET \
BQ_PROJECT_ID=YOUR_PROJECT_ID \
BQ_DATASET=stripe \
BQ_LOCATION=US \
SOURCE_FORMAT=PARQUET \
GOOGLE_SERVICE_ACCOUNT_JSON="$(cat /path/to/service-account.json)" \
node scripts/create-livemode-folder-tables.mjsOptional env vars:
LIVEMODE_PREFIX(defaultlivemode/)GCS_FILE_GLOB(default*; example*.parquet)DRY_RUN=true(prints SQL without creating tables)
This script creates/replaces external tables named from each immediate child folder under livemode/.
If your Stripe lake lands in GCS and BigQuery tables are external tables over those folders, this app now includes a 2-hour cron:
0 1-23/2 * * *->GET /api/stripe-bigquery-refresh10 */2 * * *->GET /api/stripe-upcoming-sync(trigger upcoming invoice snapshot writer)55 23 * * *->GET /api/stripe-upcoming-snapshots-cleanup(daily prune to keep latest same-day snapshot)
Required env vars for this job:
GOOGLE_SERVICE_ACCOUNT_JSON(orGOOGLE_SERVICE_ACCOUNT_JSON_BASE64)GCS_BUCKETBQ_PROJECT_ID(optional if present in service account JSON)
Optional env vars:
BQ_DATASET(defaultstripe)BQ_LOCATION(defaultUS)STRIPE_BQ_SNAPSHOT_MODE(defaultlivemode; set totestmodeif needed)STRIPE_BQ_SNAPSHOT(optional explicit snapshot like2026031309; normally auto-detected)GCS_FILE_GLOB(default*)SOURCE_FORMAT(defaultPARQUET)STRIPE_BQ_BUCKET_REFRESH_DRY_RUN(true/false, defaultfalse)
Behavior:
- Each run finds the latest snapshot folder in
gs://<GCS_BUCKET>/that contains<snapshot>/<mode>/.... - It then rebuilds external tables to point at that snapshot.
- Folder
invoice_line_itemsis mapped to tableinvoice_linesso existing app queries keep working.
If invoice discounts are stored at invoice level (not per line), use a serving view that:
- computes realized invoice discount from
invoices_viewtotals (subtotal + tax - total) - ignores discount codes that did not reduce money (
realized_discount = 0) - allocates realized discount proportionally across recurring discountable lines
SQL script (for this project):
sql/stripe_arr_serving_net_discount_v1.sql
Recommended env values:
BIGQUERY_LOCATION=northamerica-northeast1BIGQUERY_STRIPE_TABLE=orbital-lantern-330119.stripe_views.stripe_arr_serving_net_discount_v1BIGQUERY_SCHEMA_MODE=int_tsBIGQUERY_TS_UNIT=milliseconds
The GET|POST /api/billing/monthly-draft-invoices job runs at 09:05 UTC on the first day of each month. It reads Closed Won deals from HubSpot's default sales pipeline and creates Stripe invoices with auto_advance=false; it never finalizes, emails, or charges them.
The job is forced into dry-run mode until BILLING_DRAFTS_ENABLED=true is set:
BILLING_PIPELINE_ID(optional; defaults to this portal's Sales Default Pipeline internal ID,0cbbc8c6-dccf-4601-8d59-b6a15ed5129b)BILLING_DEALSTAGE(optional; defaults to this portal's Sales Default Pipeline Closed Won stage internal ID,f12c408f-f92b-4a95-8b59-9f801b19b105)BILLING_DRAFTS_ENABLED(default false)BILLING_ALLOWED_CURRENCIES(optional comma-separated allowlist; defaultUSD)BILLING_DAYS_UNTIL_DUE(optional; default30)BILLING_DAYS_UNTIL_DUE_PROPERTY(optional HubSpot deal override)BILLING_STRIPE_CUSTOMER_ID_PROPERTY(optional HubSpot deal property containing acus_...ID)BILLING_STRIPE_CUSTOMER_METADATA_KEY(optional Stripe customer metadata key; defaultworkspace_id)BILLING_MAX_DEALS_PER_RUN(optional; default and maximum1000)
Every deal in the configured Closed Won stage and pipeline is checked, but a draft is created only when at least one associated line item is due in the requested month. If the deal has no explicit Stripe customer ID, the job matches DEAL_WORKSPACE_ID_PROP to Stripe customer metadata. Missing or ambiguous matches are skipped. Recurring line items must have a start date, a supported monthly/quarterly/semiannual/annual frequency, a positive amount, and either an end date or term. One-time items are included only in their start month.
Manual dry-run example:
curl -H "Authorization: Bearer $CRON_SECRET" \
"https://YOUR_DOMAIN/api/billing/monthly-draft-invoices?month=2026-09&dryRun=true"GET|POST /api/customer-monthly-history-sync rebuilds
botpress-stripe-data-pipeline.precomputed_tables.customer_monthly_history every day at 10:30 UTC,
after the website's nightly precomputed-facts refresh. GET requires Authorization: Bearer $CRON_SECRET;
signed-in admins can also trigger it with POST.
The endpoint submits the rebuild as an asynchronous BigQuery job and returns HTTP 202 with its
jobId; the existing table remains available until BigQuery atomically replaces it on completion.
The table has one row per logical customer per calendar month, from the later of the customer's signup
month or CUSTOMER_MONTHLY_HISTORY_START through the current month. It includes customers with zero ARR.
Core columns are:
- identity:
customer_month_key,month_start,customer_key,customer_id,stripe_customer_ids - workspace/account:
workspace_id,workspace_ids,hubspot_company_id,customer_name,email,signup_date,deployment_type,deployment_types - monthly state:
motion,pricing_plan,plan_family,plan_version,billing_interval,active_pricing_plans,arr,mrr,is_active
ARR and motion use vw_fact_customer_arr_periodic_current, so they stay aligned with the website's
combined-subscriptions heuristics. Stripe customer metadata connects customer IDs to workspaces and
HubSpot accounts. Stripe MRR events plus price/product metadata add detailed labels such as
v3 plus annual and v4 plus monthly. For Stripe customers that do not yet exist in the website fact,
the job derives ARR directly from the cumulative Stripe MRR event history and classifies them as
selfserve; HubSpot accounts default to salesled. Sales-assist flags from the website fact override
both defaults.
The customer population is not restricted to Cloud deployments. Every non-archived, Closed Won
HubSpot company is included regardless of deployment type, and all observed deployment types are
retained in deployment_types. Stripe customers are also included regardless of deployment type.
Optional configuration:
CUSTOMER_MONTHLY_HISTORY_PROJECT(defaultPRECOMPUTED_TABLES_PROJECT, thenbotpress-stripe-data-pipeline)CUSTOMER_MONTHLY_HISTORY_DATASET(defaultPRECOMPUTED_TABLES_DATASET, thenprecomputed_tables)CUSTOMER_MONTHLY_HISTORY_TABLE(defaultcustomer_monthly_history)CUSTOMER_MONTHLY_HISTORY_START(default2015-01-01)STRIPE_SOURCE_PROJECT/STRIPE_SOURCE_DATASET(fallback source location)- existing
BIGQUERY_STRIPE_*source-table overrides are honored
Manual run:
curl --fail-with-body \
-H "Authorization: Bearer $CRON_SECRET" \
"https://YOUR_DOMAIN/api/customer-monthly-history-sync"HubSpot:
HUBSPOT_PRIVATE_APP_TOKENINCLUDED_DEALSTAGEHUBSPOT_TRANSACTIONAL_STAGE_ID(optional, used by sales-assist matching on Stripe Through MRR; overrides label lookup)HUBSPOT_COMMISSION_TRANSACTIONAL_PIPELINE_ID/HUBSPOT_COMMISSION_TRANSACTIONAL_CLOSED_WON_STAGE_ID(optional commissions pipeline overrides)HUBSPOT_COMMISSION_SALES_PIPELINE_ID/HUBSPOT_COMMISSION_SALES_CLOSED_WON_STAGE_ID(optional commissions pipeline overrides)HUBSPOT_PORTAL_ID(optional, used for deal links on the commissions page)COMMISSION_CLAWBACK_LOOKBACK_MONTHS(optional, defaults to24)HUBSPOT_TRANSACTIONAL_STAGE_LABEL(optional fallback label, defaultClosed Won (Transactional Pipeline))FX_TARGET_CURRENCYHUBSPOT_CONTRACTED_ARR_FIELD(optional, defaultcontracted_arr)HUBSPOT_CURRENT_ARR_FIELD(optional)HUBSPOT_ARR_SYNC_DATE_FIELD(optional)
Authentication (Google SSO):
AUTH_SECRET(required for Auth.js session signing)GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETAUTH_ALLOWED_DOMAINS(comma-separated company domains, e.g.botpress.com,example.com)AUTH_ALLOWED_EMAILS(optional comma-separated Viewer email allowlist)AUTH_ADMIN_EMAILS(optional comma-separated admin email allowlist)AUTH_SALES_EMAILS(optional comma-separated Sales email allowlist)AUTH_ACCOUNT_MANAGEMENT_EMAILS(optional comma-separated Account Management email allowlist)AUTH_GTM_EMAILS(optional comma-separated GTM email allowlist; role lists may overlap)
Stripe:
STRIPE_SECRET_KEYSTRIPE_INVOICE_STATUS(optional, defaultpaid)STRIPE_TARGET_CURRENCY(optional, defaultUSD)ARRP_READ_WRITE_TOKENorAARP_READ_WRITE_TOKENorBLOB_READ_WRITE_TOKEN(required for persistent sync store)
QuickBooks:
QUICKBOOKS_CLIENT_IDQUICKBOOKS_CLIENT_SECRETQUICKBOOKS_REDIRECT_URI
CRON_SECRET(recommended)CURRENCYLAYER_ACCESS_KEY(required for Currencylayer FX conversion on the secondary CAC chart)CURRENCYLAYER_BASE_URL(optional, defaulthttps://api.currencylayer.com)STRIPE_LINE_FETCH_CONCURRENCY(default8)STRIPE_REPORT_CACHE_TTL_MS(default300000)STRIPE_REPORT_AUTO_SYNC(defaultfalse)STRIPE_SYNC_FRESHNESS_MS(default900000)STRIPE_SYNC_MAX_HISTORY_DAYS(default800)STRIPE_SYNC_MAX_INVOICES_PER_RUN(default120)STRIPE_SYNC_CRON_ITERATIONS(default12)STRIPE_SYNC_MAX_RUNTIME_MS(default40000)STRIPE_SYNC_STORE_PATH(local fallback path, default/tmp/arr-stripe-sync-store.json)BLOB_STORE_ACCESS(defaultprivate; setpublicif your Blob store is still public)