Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
419 lines (348 loc) · 18.5 KB
/
Copy path.env.example
File metadata and controls
419 lines (348 loc) · 18.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# =========================================================
# 🌐 Server Configuration
# =========================================================
# development | test | production. The server refuses to boot with any other value.
NODE_ENV=development
PORT=8080
# Express "trust proxy": the number of reverse-proxy hops in front of the API (1 behind a single
# nginx), or a comma-separated list of proxy IPs/CIDRs (e.g. 10.0.0.0/8,loopback). Required in
# production; true/false are refused there (true trusts spoofed X-Forwarded-For, false/unset puts
# every client behind the proxy's one IP for rate limits and OTP throttles).
# Cloudflare -> nginx -> API: 1 is right only if nginx restores the visitor's address, i.e. it has
# `real_ip_header CF-Connecting-IP;` and a `set_real_ip_from` line for each Cloudflare range
# (https://www.cloudflare.com/ips/), and forwards that address in X-Forwarded-For. Without it the
# API sees a Cloudflare edge address as the client, and everyone behind one edge shares the rate
# limit and OTP buckets. The other way: TRUST_PROXY=2, with nginx accepting only Cloudflare ranges.
TRUST_PROXY=1
# Only for a production API with no proxy in front at all: set DIRECT_EXPOSURE=1 AND TRUST_PROXY=0.
DIRECT_EXPOSURE=
CLUSTERING_ENABLED=true
# Cluster worker count. Required in production (integer >= 1); falls back to the CPU count elsewhere.
WEB_CONCURRENCY=2
SERVER_WORKERS=
# Background jobs: all (default when empty) = every worker schedules them, Redis locks pick one
# runner per tick; none = API-only replica that schedules no jobs. Any other value fails the boot.
CRON_ROLE=
# Max in-memory entries (leave empty for default)
MAX_MEMORY_ENTRIES=
# Allowed CORS origins (comma-separated). Also better-auth trustedOrigins. Required in production,
# where every entry must be https:// and not localhost.
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:8080
# =========================================================
# 🗄 Database (Neon / Postgres)
# =========================================================
# Runtime connection string (the app's pool). On Neon this is normally the "-pooler" host.
DATABASE_URL=
# Direct (non-pooled) connection for `prisma migrate deploy`: migrations take an advisory lock
# that does not survive a transaction pooler. Leave empty when DATABASE_URL is already direct.
DIRECT_DATABASE_URL=
# Empty database used only by `migrate dev` and the drift guard (`npm run db:drift`), which replay
# the migration history into it. Local/CI only; never a production database.
SHADOW_DATABASE_URL=
# Connection budget for ONE replica (one server process and its WEB_CONCURRENCY workers), not per
# worker. Each worker opens its own pool of max(2, DB_POOL_MAX_TOTAL / workers) connections. Keep
# DB_POOL_MAX_TOTAL x replicas at or under about 80% of Postgres' max_connections (README,
# "Database connections"); production refuses to boot when the derived pools overcommit it.
DB_POOL_MAX_TOTAL=80
# Optional explicit per-process override. Leave unset unless a connection pooler (PgBouncer in
# transaction mode) sits in front — a stale value here is how you overcommit the database.
# Optional, PgBouncer-only; when set it must be an integer >= 1.
DB_POOL_MAX=
DB_POOL_IDLE_TIMEOUT_MS=15000
DB_POOL_CONNECTION_TIMEOUT_MS=30000
DB_POOL_STATEMENT_TIMEOUT_MS=30000
# Opt-in, no default: Postgres ends a session left idle inside an open transaction after this
# long (V-115). It is sent as a connection startup parameter, which Neon's -pooler (PgBouncer) can
# refuse, failing every connection. Set it only for a direct Postgres; behind a pooler run
# ALTER ROLE <role> SET idle_in_transaction_session_timeout = '60s'; instead (README).
# DB_IDLE_IN_TRANSACTION_TIMEOUT_MS=60000
# =========================================================
# ⚡ Redis (Cache, Sessions, Rate Limiting & Job Locks)
# =========================================================
# Required in production (set explicitly, redis:// or rediss://). Localhost is fine: a Redis bound
# to loopback on the same server is normal. With compose, leave REDIS_URL unset and set
# REDIS_PASSWORD: Redis is started with --requirepass and REDIS_URL defaults to
# redis://:${REDIS_PASSWORD}@redis:6379.
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=
# compose only: Redis memory ceiling (--maxmemory, policy volatile-lru). Default 256mb.
REDIS_MAXMEMORY=256mb
# Worker boot gives up on Redis after this long; a wrong password exits at once (default 15000).
REDIS_BOOT_TIMEOUT_MS=15000
# =========================================================
# 🔌 API Keys (Optional Integrations)
# =========================================================
# Note: the code treats an UNSET (or blank) value as true; set it explicitly. Without
# GITHUB_OWNER/GITHUB_REPO the sync is not scheduled and logs one warning at boot.
GITHUB_SYNC_ENABLED=false
GITHUB_TOKEN=
# =========================================================
# 🔐 Security & Rate Limiting
# =========================================================
# Per-route rate limiting (15 minutes window). Keyed by (method, path, ip), so this bounds each
# endpoint independently — it is not a cap on a client's total load.
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# Coarse per-IP ceiling underneath the per-route limits above, so one client cannot issue
# RATE_LIMIT_MAX_REQUESTS against every endpoint separately. Should sit well above any real
# single-user session.
GLOBAL_RATE_LIMIT_WINDOW_MS=900000
GLOBAL_RATE_LIMIT_MAX_REQUESTS=1000
# Auth route rate limiting
AUTH_RATE_LIMIT_WINDOW_MS=60000
AUTH_RATE_LIMIT_MAX_REQUESTS=20
# How long the limiter waits for Redis before counting in process memory instead (fail-closed
# classes answer 503). A stalled Redis must not stall every request; after 3 timeouts in a row
# the limiter skips Redis for 5 s.
RATE_LIMIT_REDIS_TIMEOUT_MS=400
# Longest wait for any other Redis reply before the caller falls back (default 500).
REDIS_COMMAND_TIMEOUT_MS=500
# Privacy-safe anonymous analytics visitor identifier pepper
# Production refuses this example value.
ANALYTICS_HASH_PEPPER=veriworkly-analytics-pepper
# =========================================================
# 🪵 Logging
# =========================================================
LOG_LEVEL=info
# Deprecated: only the fallback for RETENTION_AUDIT_LOG_DAYS below, which wins when set.
AUDIT_LOG_RETENTION_DAYS=90
# =========================================================
# 🧹 Data retention (daily at 03:15 UTC; 0 disables a rule)
# =========================================================
# Delete Session rows this many days after they expired.
RETENTION_EXPIRED_SESSION_DAYS=1
# Delete Verification rows (OTPs, email links) this many days after they expired.
RETENTION_EXPIRED_VERIFICATION_DAYS=1
# Replace PROCESSED billing webhook payloads with {redacted,type} after this many days; FAILED and
# PROCESSING rows keep theirs until the next setting.
RETENTION_WEBHOOK_PAYLOAD_DAYS=30
# The same for FAILED and PROCESSING rows (no replay is left to do this late); the product, amount,
# payment id, status and error columns stay.
RETENTION_UNPROCESSED_WEBHOOK_PAYLOAD_DAYS=180
# Delete request AuditLog rows (one per 4xx/5xx response) older than this.
RETENTION_AUDIT_LOG_DAYS=90
# Delete AdminAuditEntry rows (admin actions) older than this, other moderation entries included (a
# document delete or restore, a share revoke). Money actions (billing.*, affiliate commission,
# withdrawal, forfeited-balance and tier changes) follow the next setting. Takedown records are never
# deleted (owner decision 11): document visibility decisions, portfolio force-unpublishes, and
# portfolio status changes into or out of SUSPENDED.
RETENTION_ADMIN_AUDIT_DAYS=365
# Delete admin money-action entries older than this (7 years).
RETENTION_ADMIN_MONEY_AUDIT_DAYS=2555
# Delete COMMITTED/RELEASED/EXPIRED credit reservations this many days after they settled. Keep it
# long: the row is reserve()'s idempotency record for its requestId, and a retry arriving after it
# is gone runs the AI call again and then fails to commit (500).
RETENTION_CREDIT_RESERVATION_DAYS=90
# Clear the stored AI result of a paid request (what a retry after a lost response is answered
# from) this many days after it was charged. After that a retry with the same requestId gets 409.
# 0 turns the clearing off: the stored result (the user's AI output) then stays until the
# reservation row itself is deleted after RETENTION_CREDIT_RESERVATION_DAYS.
RETENTION_CREDIT_REPLAY_DAYS=1
# Delete ViewFlushBatch and UsageMetricFlushBatch idempotency markers older than this.
RETENTION_FLUSH_BATCH_DAYS=7
# Clear the IP and user-agent hashes of AffiliateClick rows older than this. The row stays, so the
# affiliate dashboard and admin click totals do not shrink.
RETENTION_AFFILIATE_CLICK_DAYS=180
# Fold PortfolioViewDaily rows whose day is older than this into one lifetime row per portfolio and
# referrer host. Total views and referrer totals stay; the day-by-day history goes.
RETENTION_PORTFOLIO_VIEW_DAYS=180
# Replace the content of documents soft-deleted more than this many days ago with {"purged":true}.
# The row stays; a purged document can never be restored, whatever this is set to later.
RETENTION_DELETED_DOCUMENT_DAYS=30
# =========================================================
# 🤖 AI Provider
# =========================================================
AI_API_KEY=
AI_BASE_URL=
# Model ids are not environment variables: each private policy file below names its own models.
# Three independent private policy files — point each _PATH to an ignored/mounted file, or
# inject the same JSON inline through the matching _JSON var. Keeping them separate makes it
# obvious which policy governs which feature; none of them are safe to commit.
# 1) General AI writing actions (rewrite/generate/tailor) — model routing, prompts, credit costs.
# e.g. ./.private/ai-actions-policy.json
AI_ACTIONS_POLICY_PATH=
AI_ACTIONS_POLICY_JSON=
# 2) ATS AI-analysis layer (paid /ats/analyze insights + resume conversion) — prompts and pricing.
# e.g. ./.private/ats-ai-policy.json
ATS_AI_POLICY_PATH=
ATS_AI_POLICY_JSON=
# 3) ATS deterministic scoring engine — rule weights, regex patterns, keyword synonym/phrase
# dictionary. This is the part that must never be public, or the scoring becomes gameable.
# Must be built for the engine version this server runs; in production the server refuses
# to start with a policy its engine rejects. e.g. ./.private/ats-engine-policy.json
ATS_ENGINE_POLICY_PATH=
ATS_ENGINE_POLICY_JSON=
# Resident-memory cap for the resume-extraction child; past it the upload fails with 422.
ATS_EXTRACT_MAX_RSS_MB=512
AI_TIMEOUT_MS=45000
AI_RATE_LIMIT_WINDOW_MS=60000
AI_RATE_LIMIT_MAX_REQUESTS=20
SITE_URL=https://veriworkly.com
# Calls the site's /api/revalidate after a changelog sync or a roadmap edit, so veriworkly.com
# shows it within seconds. Same value as the site's SITE_REVALIDATE_SECRET, at least 32
# characters. Empty: off, and the site refreshes on its weekly timer.
SITE_REVALIDATE_SECRET=
# =========================================================
# 🔑 Authentication (Better-Auth)
# =========================================================
# Production refuses the example value and anything shorter than 32 characters.
AUTH_SECRET='dev-auth-secret-1234567890'
# Base URL for auth callbacks
# Required in production (https, not localhost).
AUTH_BASE_URL=http://localhost:8080
# Extra Host names the auth routes answer to, comma-separated (V-155). AUTH_BASE_URL's host and
# loopback are always accepted; any other Host header gets 400 HOST_NOT_ALLOWED.
ALLOWED_HOSTS=
# Frontend Studio base URL and login URL (used for auth error redirects)
STUDIO_URL=http://localhost:3001
STUDIO_LOGIN_URL=http://localhost:3001/login
# Cookie domain for shared subdomains (e.g. .veriworkly.com)
AUTH_COOKIE_DOMAIN=
# Headers consulted for IP address resolution
AUTH_IP_ADDRESS_HEADERS=x-client-ip,x-forwarded-for,x-real-ip,cf-connecting-ip
# Session TTL (30 days)
AUTH_SESSION_TTL_SECONDS=2592000
# Reset TTL on activity (seconds)
AUTH_SESSION_RESET_TTL_ON_USE=86400
# Server session cache and revocation tombstone lifetime (seconds). better-auth's cookie
# cache is always off so a revoked session stops on the next request (V-369).
AUTH_SESSION_CACHE_MAX_AGE_SECONDS=900
# OTP expiry (seconds)
AUTH_OTP_TTL_SECONDS=300
# Max OTP attempts
AUTH_OTP_ALLOWED_ATTEMPTS=3
# OAuth credentials (enabled when both client ID and secret are set)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
# =========================================================
# 📧 Email Configuration
# =========================================================
# Required in every environment: the email address of the operator account (admin panel, the
# admin-only production gates, contact-form alerts). One plain address; the boot refuses anything
# else. The account is created on first boot if it does not exist.
ADMIN_EMAIL=admin@example.com
AUTH_EMAIL_FROM=
# Email provider: "resend" (recommended) or "smtp" in production; "console" in development
AUTH_EMAIL_PROVIDER=
# Option 1: Resend API (recommended)
# Get your API key from https://resend.com/api-keys
RESEND_API_KEY=
# Option 2: SMTP settings (Hostinger, Resend SMTP, SendGrid, etc.)
# For Resend SMTP: host=smtp.resend.com, port=465 (secure=true) or 587 (secure=false), user=resend, pass=<RESEND_API_KEY>
AUTH_SMTP_HOST=
AUTH_SMTP_PORT=
AUTH_SMTP_SECURE= # true | false
AUTH_SMTP_USER=
AUTH_SMTP_PASS=
# =========================================================
# 📊 Usage Metrics
# =========================================================
# Cron schedule (daily at 00:10)
USAGE_METRICS_FLUSH_CRON=10 0 * * *
USAGE_METRICS_FLUSH_TIMEZONE=UTC
USAGE_METRICS_REDIS_RETENTION_DAYS=10
# =========================================================
# 🔐 Internal server-to-server keys (sent as the x-internal-key header)
# =========================================================
# Marks a POST /api/v1/stats/events call as internal: custom_* events and a value are accepted
# (at most 50 distinct custom names per day). Unset = no internal stats callers. Production: 32+ chars.
STATS_INTERNAL_KEY=
# Lets a trusted server-side caller (studio/portfolio SSR) name the browser's IP in
# x-forwarded-client-ip, used for rate limiting and audit. Without the key the header is
# ignored. Unset = disabled. Production: 32+ chars. Generate with: openssl rand -hex 32
INTERNAL_API_KEY=
# =========================================================
# ⚡ Cache TTLs (seconds)
# =========================================================
ROADMAP_CACHE_TTL_SECONDS=2592000
ROADMAP_STATS_CACHE_TTL_SECONDS=2592000
GITHUB_STATS_CACHE_TTL_SECONDS=43200
CHANGELOG_CACHE_TTL_SECONDS=2592000
# =========================================================
# 🐙 GitHub Sync (Scheduler)
# =========================================================
GITHUB_OWNER=
GITHUB_REPO=
GITHUB_PROJECT_URL=
# Runs twice daily
GITHUB_SYNC_CRON=0 0,12 * * *
GITHUB_SYNC_TIMEZONE=UTC
# Internal API protection key
INTERNAL_SYNC_API_KEY=
# =========================================================
# 📝 Changelog Release Sync (Scheduler)
# =========================================================
CHANGELOG_RELEASE_SYNC_ENABLED=true
CHANGELOG_RELEASE_SYNC_CRON=0 6 * * *
CHANGELOG_RELEASE_SYNC_TIMEZONE=UTC
CHANGELOG_RELEASE_SYNC_MIN_INTERVAL_SECONDS=21600
# =========================================================
# 🌐 Portfolio Subdomain Builder
# =========================================================
# Required in production (https, not localhost).
PORTFOLIO_URL=http://localhost:3004
# Production refuses this development value.
PORTFOLIO_REVALIDATE_SECRET=dev-revalidate-secret
# =========================================================
# 💳 Billing (Dodo Payments: plans, passes and credit packs)
# =========================================================
PORTFOLIO_GRACE_DAYS=7
DODO_PAYMENTS_API_KEY=
# Required in every environment: an unsigned webhook mints credits and entitlements for whoever
# posts it. The only exception is local development with ALLOW_UNSIGNED_WEBHOOKS=1 below.
DODO_PAYMENTS_WEBHOOK_SECRET=
# Local development only: with an empty webhook secret, NODE_ENV != production AND this set to 1,
# unsigned JSON posted from the loopback interface is accepted. Never set it on a server.
ALLOW_UNSIGNED_WEBHOOKS=
DODO_PAYMENTS_ENVIRONMENT=test_mode
DODO_PAYMENTS_PORTFOLIO_PRO_MONTHLY_PRODUCT_ID=
DODO_PAYMENTS_PORTFOLIO_PRO_ANNUAL_PRODUCT_ID=
DODO_PAYMENTS_BUNDLE_ONE_DAY_PRODUCT_ID=
DODO_PAYMENTS_BUNDLE_SEVEN_DAY_PRODUCT_ID=
DODO_PAYMENTS_AI_CREDITS_MONTHLY_PRODUCT_ID=
DODO_PAYMENTS_AI_CREDITS_ANNUAL_PRODUCT_ID=
DODO_PAYMENTS_BUNDLE_MONTHLY_PRODUCT_ID=
DODO_PAYMENTS_BUNDLE_ANNUAL_PRODUCT_ID=
DODO_PAYMENTS_CREDIT_PACK_250_PRODUCT_ID=
DODO_PAYMENTS_CREDIT_PACK_500_PRODUCT_ID=
DODO_PAYMENTS_CREDIT_PACK_1200_PRODUCT_ID=
# Credit pack prices in USD cents (defaults 299 / 499 / 999): must match the price set for the product in the Dodo dashboard.
CREDIT_PACK_250_PRICE_CENTS=299
CREDIT_PACK_500_PRICE_CENTS=499
CREDIT_PACK_1200_PRICE_CENTS=999
# The three URLs below are required in production (https, not localhost); the code defaults point
# at localhost.
DODO_PAYMENTS_CHECKOUT_RETURN_URL=http://localhost:3001/billing?checkout=complete
DODO_PAYMENTS_CHECKOUT_CANCEL_URL=http://localhost:3001/billing?checkout=cancelled
DODO_PAYMENTS_PORTAL_RETURN_URL=http://localhost:3001/billing
# =========================================================
# ☁️ Portfolio Media (Cloudflare R2)
# =========================================================
R2_ENDPOINT=
R2_BUCKET=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_PUBLIC_BASE_URL=
# =========================================================
# 🚪 API Key Configuration
# =========================================================
# Required in production, must differ from AUTH_SECRET; production refuses this example value.
API_KEY_HASH_SECRET=dev-api-key-secret
API_KEY_AUTH_CACHE_TTL_SECONDS=300
API_KEY_LAST_USED_TOUCH_INTERVAL_SECONDS=300
API_KEY_DEFAULT_RATE_LIMIT=20
API_KEY_MAX_RATE_LIMIT=600
API_KEY_DEFAULT_SCOPES=user:read
API_KEY_DEFAULT_LIFETIME_DAYS=365
# =========================================================
# 🚩 Growth Programs (Affiliate & Campus Ambassador)
# =========================================================
# Blank or unset: on in development and test, off in production. Set true or false to choose.
AFFILIATE_PROGRAM_ENABLED=
AMBASSADOR_PROGRAM_ENABLED=
# Days after sign-up during which a referral code can still be applied (1-365, default 30).
AFFILIATE_ATTRIBUTION_DAYS=30