_private/qwestly-docs/Engineering/environment-variables-audit-2026.md
Table of Contents
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.copyfiles rewritten (candidate, hire, internal, api-python, agent, candidate-catalog) - 1
.env.copycreated from scratch (public-site — had none) - 2 leaked secrets fixed —
VERCEL_AUTOMATION_BYPASS_SECRETwas in plaintext in bothcandidate/.env.copyand_internal/candidate-catalog/.env.copy - 1 code bug fixed —
system-info/route.tswas checkingVAPI_PUBLIC_KEY(dead var) instead ofNEXT_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 |
|
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_TOKENwas already flagged for removal.
Per-app env var inventory
candidate (candidate-qwestly.vercel.app → app.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 | VERCEL_BYPASS_TOKEN aliasVERCEL_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_SECREThas been removed from all apps and Vercel.AUTH0_SECRETis 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 refreshprofile.service.ts:104— decodes token for profile operationsprovisioned-account.ts:208,241— encodes/decodes for provisioned account setupsession-check/route.ts:31— decodes token for session validationadmin/auth/route.ts:34— decodes admin auth tokenwaitlist.ts:744— decodes token for waitlist authsystem-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 loginauth/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 loginauth/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) readsNEXT_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 alwaysfalsein the system info panel even when a valid key was configured.
Security: leaked secrets removed from .env.copy files
candidate/.env.copy:65— had liveVERCEL_AUTOMATION_BYPASS_SECRETvalue_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 |