Environment reference

Every environment variable the social pipeline reads — what sets it, its default, and whether it is currently configured in production.

Environment reference

Every variable the social pipeline actually reads, extracted from the source rather than from memory. Defaults are what the code falls back to when a variable is unset — several of them are load-bearing.

Production column reflects 2026-07-30, read from vercel env ls (names only), and matches Status.

Core

VariableDefaultRequiredProdWhat it does
CRON_SECRETYesBearer secret for the cron, drain, metrics, queue, relay, and approval-signing. Whoever holds it can publish — treat as a publishing credential.
DATABASE_URLYesNeon Postgres — the ledger (SocialPiece/SocialVariant/SocialMetric) behind scheduling, single-use approvals, the drain, and metrics. The Prisma client constructs lazily, so a build never needs it.
SOCIAL_PUBLIC_URLhttps://kun.databayt.orgNoBase URL for approval links. Falls back to the canonical production origin — deliberately never the request's Host header, which is attacker-controlled.

CRON_SECRET must exist in two places. The routes read it from Vercel env; the drain and metrics workflows read it from a GitHub repo secret of the same name (gh secret set CRON_SECRET). Missing the GitHub copy, both scheduled lanes self-disable with a ::warning:: on every run — quietly, by design.

Drafting

VariableDefaultRequiredProdWhat it does
SOCIAL_AUTOPOST_PRODUCTSemptyFor the cronComma-separated brands opted into auto-drafting. Empty means the cron does nothing — this is why it is currently a no-op.
SOCIAL_DRAFT_SOURCEhermesNohermes hands the request to the gateway; anthropic drafts server-side. The default dead-ends on a cloud deployment, since Hermes is not reachable from it.
SOCIAL_DRAFT_FALLBACKunsetNoSet to anthropic to draft server-side only when Hermes is down, instead of skipping the run. Opt-in for the same billing reason — an outage never silently spends.
SOCIAL_DRAFT_MODELclaude-opus-4-8NoModel used when the source is anthropic (or the fallback fires).
SOCIAL_DRAFT_LOCALEarNoar or en. Arabic-first is the house default.
ANTHROPIC_API_KEYIf source is anthropicServer-side drafting. Costs money per draft — an explicit spend decision, not a config tweak. Unfunded (401) — see D-20260730.
GEMINI_API_KEYFor inline draftingThe inline lane and drain-google: AI SDK generateObject on gemini-3.6-flash, free tier (measured 20 requests/day/model — D-20260807). Absent, every ask queues for the Mac drain.
SOCIAL_DRAFT_INLINEunset (= on)Nooff reverts the agent window to queue-only drafting — D-20260807's one-variable revert. Anything else means inline ON.

Review destination

The draft-and-approve loop needs somewhere private to send drafts. It tries Hermes/Slack first, then Telegram. If neither is configured, the cron fails after drafting.

VariableDefaultRequiredProdWhat it does
SOCIAL_REVIEW_CHANNELslackNoWhich channel Hermes relays drafts into.
TELEGRAM_REVIEW_CHAT_IDFallbackA private chat id. Must never be the public brand channel — drafts carry one-click publish links.

Telegram

VariableDefaultRequiredProdWhat it does
TELEGRAM_BOT_TOKENYesFrom BotFather. Present locally, never pushed to production.
TELEGRAM_CHANNEL_IDYesPublic brand channel, @handle or -100…. Does not exist anywhere yet.

Telegram setup →

Facebook

Per brand, with the product id uppercased (HOGWARTS, MKAN, DATABAYT, …):

VariableRequiredProdWhat it does
FACEBOOK_PAGE_ID_<PRODUCT>Yes✅ ×3The Page to post to
FACEBOOK_PAGE_ACCESS_TOKEN_<PRODUCT>Yes✅ ×3Permanent Page token (expires_at: 0), insights scopes included since 2026-07-27
FACEBOOK_PAGE_IDLegacyUnsuffixed hogwarts fallback in code only — deleted from Vercel 2026-07-27 so a stale token can never silently reactivate
FACEBOOK_PAGE_ACCESS_TOKENLegacy"

Configured for hogwarts, mkan, databayt. Not for sijillee or Moallimee (registry id moalimee, single L — it is the env-var suffix FACEBOOK_PAGE_ID_MOALIMEE) — no Page exists.

Facebook setup →

Hermes gateway

VariableDefaultRequiredProdWhat it does
HERMES_API_URLFor the Hermes laneGateway endpoint. Only meaningful where Hermes is reachable — i.e. local dev, never a cloud deployment.
HERMES_API_KEYNoOptional bearer for that direction.
NEXT_PUBLIC_HERMES_API_URLNoDisplay-only, shown in the status panel. The only browser-exposed value here — never put a token in a NEXT_PUBLIC_* variable.

Slack tokens (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_HOME_CHANNEL) belong on the Hermes machine, not in the web app. Slack setup →

Not yet wired

Named here so the shape is predictable when these channels land:

ChannelVariables
InstagramINSTAGRAM_ACCOUNT_ID_<PRODUCT> (reuses the Facebook Page token)
LinkedInLINKEDIN_ORG_URN_<PRODUCT>, LINKEDIN_ACCESS_TOKEN_<PRODUCT>, LINKEDIN_REFRESH_TOKEN_<PRODUCT>, LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET
XX_API_KEY, X_API_SECRET, X_ACCESS_TOKEN_<PRODUCT>, X_ACCESS_TOKEN_SECRET_<PRODUCT>

Rules

Server-side only. The single browser-exposed variable is NEXT_PUBLIC_HERMES_API_URL, which is a URL for a status display. Every token stays server-side. A NEXT_PUBLIC_ token is a public token.

Trim on read. Vercel environment variables can carry a trailing newline that survives copy-paste and breaks header and URL construction in ways that are painful to debug. Every read in this codebase calls .trim() — do the same.

One central .env locally. No .env.local, no .env.development.

Per-brand, never shared. One leaked token should compromise one brand, not all of them.

Quickest path from nothing to a working loop

Given a deployment that already has CRON_SECRET and Facebook configured, three variables bring the entire draft-and-approve loop alive:

TELEGRAM_BOT_TOKEN=...          # from BotFather
TELEGRAM_CHANNEL_ID=@yourbrand  # the public channel
TELEGRAM_REVIEW_CHAT_ID=-100... # a PRIVATE chat, not the above

Then opt a brand in and pick a reachable draft source:

SOCIAL_AUTOPOST_PRODUCTS=databayt
SOCIAL_DRAFT_SOURCE=anthropic   # spend decision — the default is unreachable from the cloud

anthropic is a billing decision, not a config tweak. The house billing doctrine is subscription-only (Claude Max, no API-key spend), so flipping the cron's draft source to anthropic — or setting SOCIAL_DRAFT_FALLBACK — still needs /decide + Abdout's sign-off. Everything unattended stays no-spend: SOCIAL_AUTOPOST_PRODUCTS empty, source defaults to hermes. The zero-spend drafting path remains a Claude Code session (/draft, Max pool).

One surface was decided and then blocked. D-20260730 sanctioned the Social Agent window on /social drafting through the key per press (~$0.02–0.05/draft, SOCIAL_DRAFT_MODEL as the cost knob). A live production test the same day returned 401 invalid x-api-key: the key in Vercel has been rejected since it was added, which is what subscription-only billing predicts — there are no credits for a key to spend. So no ANTHROPIC_API_KEY value in this table is currently funded. The window ships on the queue instead: it records a SocialDraftRequest and a Claude Code session answers it on the Max pool. Funding the lane is a separate decision that also has to clear a payment method.