_private/qwestly-docs/Engineering/environment-variables-audit-2026.md

Environment Variables Audit — June 2026

Status: In progress — simplification #1 (JWT_SITE_TOKEN_SECRET) completed. Remaining simplifications: #2-4, #6.

This audit covers all 7 active Qwestly apps. For each app we analyzed: what's in Vercel, what's in code, what's in .env.copy, and what can be pruned.


Summary of work done

  • 6 .env.copy files rewritten (candidate, hire, internal, api-python, agent, candidate-catalog)
  • 1 .env.copy created from scratch (public-site — had none)
  • 2 leaked secrets fixedVERCEL_AUTOMATION_BYPASS_SECRET was in plaintext in both candidate/.env.copy and _internal/candidate-catalog/.env.copy
  • 1 code bug fixedsystem-info/route.ts was checking VAPI_PUBLIC_KEY (dead var) instead of NEXT_PUBLIC_VAPI_PUBLIC_KEY (the one actually used by the Vapi SDK)

Shared Vercel env vars (inherited by all projects)

These are set at the team level on Vercel and propagate to all projects:

Variable Used by
OPENAI_API_KEY candidate, hire, internal, api-python, agent, candidate-catalog
AWS_ACCESS_KEY_ID candidate, hire, api-python
AWS_SECRET_ACCESS_KEY candidate, hire, api-python
AWS_REGION candidate, hire, api-python
VERCEL_AUTOMATION_BYPASS_SECRET candidate, hire, internal, agent
AUTH0_CLIENT_ID candidate, hire, internal, candidate-catalog
AUTH0_CLIENT_SECRET candidate, hire, internal, candidate-catalog
AUTH0_DOMAIN candidate, hire, internal, candidate-catalog
AUTH0_API_CLIENT_ID candidate, hire, internal, candidate-catalog
AUTH0_API_CLIENT_SECRET candidate, hire, internal, candidate-catalog
LANGSMITH_API_KEY candidate, hire, internal, api-python, agent, candidate-catalog
QWESTLY_AGENT_SHARED_SECRET candidate, agent
QWESTLY_SERVICE_API_KEY candidate, api-python, agent, candidate-catalog
DEEPSEEK_API_KEY agent
JWT_SITE_TOKEN_SECRET candidate, hire, internal
HIRE_APP_API_KEY candidate, hire
OPENAI_ORG candidate, hire, internal, api-python, agent, candidate-catalog
OPENAI_PROJECT candidate, hire, internal, api-python, agent, candidate-catalog
GH_QWESTLY_UI_TOKEN candidate, hire, internal

Note: QWESTLY_REGISTRY_GH_TOKEN was already flagged for removal.


Per-app env var inventory

candidate (candidate-qwestly.vercel.appapp.qwestly.com)

In Vercel (app-specific):

Variable Used? Notes
AGENT_API_URL Agent service URL
CANDIDATE_CATALOG_API_KEY Auth for catalog service
CANDIDATE_SEARCH_API_KEY Service-to-service candidate search
NETWORKING_ACTION_SECRET HMAC for networking emails
NEXT_PUBLIC_MARKETING_SITE_URL Marketing site origin
NEXT_PUBLIC_REDDIT_PIXEL_ID Reddit conversion tracking
REDDIT_CONVERSIONS_API_TOKEN Reddit server-side conversions
SENDGRID_WEBHOOK_SECRET Webhook verification
NEXT_PUBLIC_HIRE_URL Hire portal URL
PYTHON_API_BASE_URL api-python base URL
NEXT_PUBLIC_POSTHOG_KEY Analytics
SLACK_WEBHOOK_URL Notifications
SLACK_DEV_ALERTS_WEBHOOK_URL Dev alerts
ASANA_PROJECT_ID Asana integration
ASANA_BETA_FEEDBACK_PROJECT_ID Beta feedback project
ASANA_ACCESS_TOKEN Asana API token
LINKEDIN_CLIENT_ID LinkedIn OAuth
LINKEDIN_CLIENT_SECRET LinkedIn OAuth
RAPID_API_KEY LinkedIn profile scraping
PERPLEXITY_API_KEY AI search
LANGSMITH_TRACING Observability
LANGCHAIN_TRACING_V2 Observability
LANGSMITH_ENDPOINT Observability
LANGSMITH_PROJECT Observability
LANGCHAIN_PROJECT ⚠️ Only checked in one diagnostic route; LANGSMITH_PROJECT is preferred. Keep both for now?
MONGODB_HOST DB connection
MONGODB_DATABASE DB name
MONGODB_PASS DB password
MONGODB_USER DB user
ADMIN_EMAILS Admin access list
APP_BASE_URL (prod) Production origin
APP_BASE_URL (preview) Preview origin
AUTH0_SECRET Auth0 session
AUTH0_ISSUER_BASE_URL Auth0 issuer
NEXT_PUBLIC_VAPI_PUBLIC_KEY Vapi client SDK
NEXT_PUBLIC_VAPI_ASSISTANT_ID Vapi assistant
VAPI_PRIVATE_KEY Vapi server-side
C2C_MONGODB_URI REMOVE Zero code references
NEXT_PUBLIC_ENHANCED_DB REMOVE Zero code references
ENHANCED_DB REMOVE Zero code references
SUPABASE_JWT_SECRET REMOVE Zero code references in candidate
ALLOWED_EMAILS REMOVE Only in a comment, never read
CANDIDATE_CATALOG_WEBHOOK_URL REMOVE Zero code references
GITHUB_TOKEN REMOVE Zero code references in ANY app
VAPI_PUBLIC_KEY REMOVE Dead — code uses NEXT_PUBLIC_VAPI_PUBLIC_KEY. Fixed in system-info route.

Also needs from shared (already inherited): QWESTLY_AGENT_SHARED_SECRET, QWESTLY_SERVICE_API_KEY, HIRE_APP_API_KEY, VERCEL_AUTOMATION_BYPASS_SECRET, OPENAI_API_KEY, all AUTH0_*, LANGSMITH_API_KEY, AWS_*


qwestly-hire (hire.qwestly.com)

In Vercel (app-specific):

Variable Used? Notes
PYTHON_API_BASE_URL api-python base URL
CANDIDATE_WEBHOOK_SECRET Verify incoming candidate webhooks
NEXT_PUBLIC_CANDIDATE_APP_URL Links to candidate app
LANGSMITH_PROJECT Observability (preferred over LANGCHAIN)
LANGCHAIN_TRACING_V2 Observability
LANGSMITH_ENDPOINT Observability
ANTHROPIC_API_KEY Claude for job analysis
SLACK_WEBHOOK_URL Notifications
NEXT_PUBLIC_POSTHOG_KEY Analytics
MONGODB_URI DB connection
APP_BASE_URL (prod + preview) Origins
AUTH0_SECRET Auth0 session
AUTH0_ISSUER_BASE_URL Auth0 issuer
GITHUB_TOKEN REMOVE Zero code references
SEED_SECRET_TOKEN REMOVE Zero code references

Also needs from shared (already inherited): HIRE_APP_API_KEY, VERCEL_AUTOMATION_BYPASS_SECRET, OPENAI_API_KEY, all AUTH0_*, LANGSMITH_API_KEY, AWS_*


qwestly-internal (internal.qwestly.co)

In Vercel (app-specific):

Variable Used? Notes
AUTH0_SECRET Auth0 session
AUTH0_ISSUER_BASE_URL Auth0 issuer
APP_BASE_URL Production origin
LANGCHAIN_PROJECT Observability
MONGODB_URI DB connection
PYTHON_API_BASE_URL api-python base URL
NEXT_PUBLIC_CANDIDATE_APP_URL Links to candidate app

All vars in Vercel are used. No removals needed.

Also needs from shared (already inherited): VERCEL_AUTOMATION_BYPASS_SECRET, OPENAI_API_KEY, all AUTH0_*, LANGSMITH_API_KEY


public-site (www.qwestly.com)

In Vercel (app-specific):

Variable Used? Notes
NEXT_PUBLIC_REDDIT_PIXEL_ID Reddit tracking
NEXT_PUBLIC_CANDIDATE_APP_URL Signup links
NEXT_PUBLIC_POSTHOG_KEY Analytics
NEXT_PUBLIC_MARKETING_ORIGIN REMOVE Zero code references

Needs in Vercel (not currently set): SENDGRID_API_KEY (waitlist emails). LANDING (optional, slim mode).


api-python (api.qwestly.com)

In Vercel (app-specific):

Variable Used? Notes
MONGODB_URL DB connection (shared with candidate)
VAPI_PRIVATE_KEY Voice interview calls
MONGODB_DATABASE DB name
SENDGRID_FROM_EMAIL Default sender
LANGCHAIN_TRACING_V2 Observability
LANGSMITH_TRACING Observability
CORS_ALLOW_ORIGINS ⚠️ REMOVE Deprecated — logs warning to use CORS_ALLOW_ORIGIN_REGEX instead
LANGSMITH_ENDPOINT Observability
LANGCHAIN_PROJECT Observability
RAPID_API_KEY Company profile enrichment
SENDGRID_WEBHOOK_SECRET Inbound webhook verification
POSTHOG_KEY Analytics
AWS_BUCKET_NAME S3 bucket
SUPABASE_URL REMOVE Zero code references
SUPABASE_ANON_KEY REMOVE Zero code references
SUPABASE_SERVICE_ROLE_KEY REMOVE Zero code references
SUPABASE_JWT_SECRET REMOVE Zero code references
CONFIG_DEBUG_SECRET REMOVE Zero code references
AWS_S3_LOGS_BUCKET REMOVE Zero code references

Also needs from shared (already inherited): QWESTLY_SERVICE_API_KEY, OPENAI_API_KEY, LANGSMITH_API_KEY, AWS_*, AUTH0_*


qwestly-agent (agent.qwestly.com)

In Vercel (app-specific):

Variable Used? Notes
PYTHON_API_BASE_URL api-python base URL
LANGSMITH_PROJECT Observability
CANDIDATE_APP_URL Candidate app URL for tool calls
MONGODB_URL Candidate DB (read-only)

Needs from shared (must be inherited): QWESTLY_AGENT_SHARED_SECRET, QWESTLY_SERVICE_API_KEY, DEEPSEEK_API_KEY, OPENAI_API_KEY, VERCEL_AUTOMATION_BYPASS_SECRET


candidate-catalog (candidate-catalog.vercel.app)

In Vercel (app-specific):

Variable Used? Notes
PYTHON_API_BASE_URL api-python base URL
CANDIDATE_WEBHOOK_SECRET Verify incoming candidate webhooks
SENDGRID_API_KEY Outreach emails
CANDIDATE_PORTAL_URL Candidate portal base URL
CANDIDATE_CATALOG_API_KEY Auth for candidate portal requests
APP_BASE_URL Production origin
MONGODB_URI DB connection
AUTH0_DOMAIN Auth0
AUTH0_CLIENT_ID Auth0
AUTH0_CLIENT_SECRET Auth0
AUTH0_SECRET Auth0
AUTH0_ISSUER_BASE_URL Auth0
AUTH0_API_CLIENT_ID Auth0
AUTH0_API_CLIENT_SECRET Auth0

Needs from shared (inherited): QWESTLY_SERVICE_API_KEY, OPENAI_API_KEY

Optional (has code defaults): CANDIDATE_PORTAL_SIGNUP_BASE_URL, PROD_URL, MONGODB_DB, SENDGRID_FROM_EMAIL, SENDGRID_SANDBOX, SENDGRID_TEMPLATE_*


Vercel cleanup (recommended)

To remove from Vercel

App Variables to remove Count
candidate C2C_MONGODB_URI, NEXT_PUBLIC_ENHANCED_DB, ENHANCED_DB, SUPABASE_JWT_SECRET, ALLOWED_EMAILS, CANDIDATE_CATALOG_WEBHOOK_URL, GITHUB_TOKEN, VAPI_PUBLIC_KEY 8
qwestly-hire GITHUB_TOKEN, SEED_SECRET_TOKEN 2
public-site NEXT_PUBLIC_MARKETING_ORIGIN 1
api-python SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_JWT_SECRET, CONFIG_DEBUG_SECRET, AWS_S3_LOGS_BUCKET, CORS_ALLOW_ORIGINS 7
shared (team) QWESTLY_REGISTRY_GH_TOKEN, JWT_SITE_TOKEN_SECRET 2
TOTAL 20

To add to Vercel

App Variables to add Notes
public-site SENDGRID_API_KEY Waitlist emails won't work without it

To check

Question Owner
Is LANGCHAIN_PROJECT still needed on candidate, or is LANGSMITH_PROJECT sufficient? Engineering
Is CANDIDATE_CATALOG_API_KEY still used for service-to-service auth, or is everything on QWESTLY_SERVICE_API_KEY now? Engineering
Do any apps need NEXT_PUBLIC_POSTHOG_KEY set explicitly, or is it always hardcoded/defaulted? Engineering

Simplification opportunities

# Recommendation Impact Ticket
1 Drop JWT_SITE_TOKEN_SECRET — use AUTH0_SECRET as single source of truth for site tokens Done. Removed from code, .env.copy, .env.local across all 3 apps. Vercel shared env var also removed.
2 Standardize on LANGSMITH_PROJECT — hire, candidate, agent, api-python, catalog all use different names for the same thing Choose one (prefer LANGSMITH_PROJECT, the newer LangSmith SDK name) and drop LANGCHAIN_PROJECT. :link:
3 Standardize MongoDB URI: MONGODB_URI vs MONGODB_URL vs MONGODB_HOST/USER/PASS/DATABASE 3 different naming conventions. Pick one (MONGODB_URI) and simplify candidate's multi-part construction. :link:
4 Consolidate CANDIDATE_CATALOG_API_KEY into QWESTLY_SERVICE_API_KEY Two secrets for the same server-to-server auth between catalog and candidate. :link:
5 Drop VERCEL_BYPASS_TOKEN alias — use VERCEL_AUTOMATION_BYPASS_SECRET directly ✅ Done. Only ref was in actions/networking.ts:20.
6 Consolidate SENDGRID_SANDBOX and EMAIL_SANDBOX into one var Two env vars for the same purpose in candidate and api-python. :link:

Auth secrets: JWT_SITE_TOKEN_SECRET vs QWESTLY_SERVICE_API_KEY

Update June 2026: JWT_SITE_TOKEN_SECRET has been removed from all apps and Vercel. AUTH0_SECRET is now the single source of truth for site token JWTs. The code references below are preserved for historical context.

These two secrets serve different purposes and cannot replace each other.

JWT_SITE_TOKEN_SECRET QWESTLY_SERVICE_API_KEY
Purpose HS256 secret for user session JWTs (site tokens) Static API key for server-to-server calls
How it works After Auth0 login, the app encodes a signed JWT into a cookie. Every request decodes it via proxy.ts middleware to identify the user. One backend calls another with X-API-Key: <key> header. Simple string comparison. No JWT involved.
Shared across candidate, qwestly-hire, qwestly-internal candidate, api-python, qwestly-agent, candidate-catalog
Consumed by User-facing auth flow (cookies, sessions) Background service-to-service requests (tools, webhooks, proxies)

Where JWT_SITE_TOKEN_SECRET is used

candidate (src/services/auth/token.ts:5):

  • proxy.ts:182 — decodes site token from cookie on every request (middleware)
  • auth0.ts:54,78,204,250,289 — encodes site token after Auth0 login, authorized callbacks, session refresh
  • profile.service.ts:104 — decodes token for profile operations
  • provisioned-account.ts:208,241 — encodes/decodes for provisioned account setup
  • session-check/route.ts:31 — decodes token for session validation
  • admin/auth/route.ts:34 — decodes admin auth token
  • waitlist.ts:744 — decodes token for waitlist auth
  • system-info/route.ts:43 — diagnostic check only

qwestly-hire (src/services/token.ts:5):

  • proxy.ts:122 — decodes site token on every request (middleware)
  • auth0.ts:53,106,218,244,278 — encodes site token after Auth0 login
  • auth/authorized/route.ts:45 — decodes token for auth check

qwestly-internal (src/services/token.ts:7 — fallback after AUTH0_SECRET):

  • auth0.ts:50,63 — encodes site token after login
  • auth/me/route.ts:21 — decodes token for /api/auth/me

Fallback chain (June 2026 — simplified)

All 3 apps now use AUTH0_SECRET as the single source of truth for HS256 site token signing:

const getSecret = () => {
  const raw = AUTH0_SECRET || "";
  return new TextEncoder().encode(raw);
};

JWT_SITE_TOKEN_SECRET and JWT_SECRET have been removed from all apps. AUTH0_SECRET is shared across all projects via Vercel team env vars.


Code changes made

Bug fix: candidate/src/app/api/admin/system-info/route.ts

  • Line 46: Changed !!process.env.VAPI_PUBLIC_KEY!!process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY
  • The Vapi client SDK (lib/vapi-client.ts) reads NEXT_PUBLIC_VAPI_PUBLIC_KEY (needs the prefix because it runs in the browser), but the admin diagnostic was checking the wrong variable name. This meant the "hasVapiKey" flag was always false in the system info panel even when a valid key was configured.

Security: leaked secrets removed from .env.copy files

  • candidate/.env.copy:65 — had live VERCEL_AUTOMATION_BYPASS_SECRET value
  • _internal/candidate-catalog/.env.copy:26 — same leaked value
  • Both replaced with empty placeholders. Recommend rotating the secret in Vercel.

Files changed

File Action
candidate/.env.copy Rewritten — 17 sections, added missing vars, fixed leak
qwestly-hire/.env.copy Rewritten — 15 sections, added missing, removed dead
qwestly-internal/.env.copy Rewritten — removed dead AWS/Slack/SendGrid vars
api-python/.env.copy Rewritten — removed unused Supabase/Config vars
qwestly-agent/.env.copy Restructured with † markers for shared vars
public-site/.env.copy Created from scratch — didn't exist before
_internal/candidate-catalog/.env.copy Rewritten — expanded from 7 to 25+ vars, fixed leak
candidate/src/app/api/admin/system-info/route.ts Bug fix — VAPI_PUBLIC_KEY → NEXT_PUBLIC_VAPI_PUBLIC_KEY