Two separate systems configure DataBounty Community, and knowing which one you are looking at saves a lot of confusion:
| Where it lives | When it takes effect | Who changes it | |
|---|---|---|---|
| Environment variables | .env per app, or the process environment |
Process restart | Whoever deploys |
| Runtime settings | admin_settings table in Postgres |
Next read — no redeploy | An admin in the console |
Anything about how the platform behaves — thresholds, window sizes, karma rates, feature switches — is a runtime setting, not an environment variable. Environment variables are for wiring: where the database is, which origins may call the API, which third-party keys exist.
apps/api/src/config.ts reads 63 variables, and a further 32 are read elsewhere in
apps/api/src — in worker.ts (the sweep/dispatch intervals), lib/prisma.ts, lib/session-cookie.ts,
lib/mcp-rate-limit.ts, lib/google-auth.ts and services/. That is 95 in total, and the tables below
are the complete list: anything marked ✱ is not in apps/api/.env.example.
Two variables are deliberately outside config.ts rather than by accident, and both say so at the read
site: MEMBER_ANALYTICS_ENABLED is read at route-registration time so a test can flip it before
buildApp() without depending on module import order (routes/v1/me.ts), and the publication adapters
read their own credentials directly (lib/publication/hugging-face.ts, github.ts) to keep provider
config inside the provider.
DATABASE_URL, DIRECT_URL and SHADOW_DATABASE_URL are read by Prisma rather than config.ts — DATABASE_URL by the runtime
driver adapter in lib/prisma.ts, DIRECT_URL only by the Prisma CLI via prisma.config.ts.
SMTP_SECURE is read at config.ts:227-229: blank derives implicit TLS from the port (true only for
465), and an explicit true/1 overrides it. It is listed under Email below.
| Variable | Notes |
|---|---|
DATABASE_URL |
Postgres connection string. Read by Prisma (src/lib/prisma.ts). Nothing starts without it. |
DIRECT_URL ✱ |
Conditionally required. Migration/CLI connection, read only by the Prisma CLI (prisma.config.ts, which prefers it and falls back to DATABASE_URL); the runtime never uses it. Unset is correct for local Postgres and the bundled compose database. Required when DATABASE_URL is a transaction pooler (Supabase port 6543, pgbouncer=true): that pooler cannot hold the session-scoped advisory lock Prisma Migrate takes, and the failure mode is not an error — prisma migrate hangs with nothing useful in the log (observed: no output after 10 minutes). Point it at the session pooler (5432). |
SHADOW_DATABASE_URL ✱ |
unset |
That is genuinely the only variable with no fallback. Everything below has a default — including several that must not keep their default in production. Read the next section before deploying.
These have hardcoded dev defaults published in this repository. In production the server now refuses to start on every one of them — see "Refused at boot in production" below. In development they keep their fallback so a fresh clone runs with no configuration.
| Variable | Dev fallback | What an attacker does with the default |
|---|---|---|
SESSION_SECRET |
dev_session_secret_databounty_community_32bytes_long |
Forges any member's session cookie. |
ADMIN_SESSION_SECRET |
dev_admin_session_secret_databounty_community_32bytes |
Forges an admin-console session. |
POOL_SAMPLING_HMAC_SECRET ✱ |
dev_pool_sampling_hmac_secret_databounty_community |
Predicts which items the human-audit sampler will select, so submissions can be timed to avoid review. |
PROFILE_SOURCE_SECRET ✱ |
dev_profile_source_secret_databounty_community_32b |
Forges credential-verification state. |
Set all four secrets above to distinct high-entropy values — openssl rand -hex 32. A production
deployment missing any of them fails fast with a message naming the variable, instead of running with a
publicly known key and no symptom.
NODE_ENV is treated as production for anything other than development or test — not an allowlist
of specific production-sounding names. A misspelled or unlisted value (e.g. staging) still triggers every
guard below; only development and test are exempt. These throw on startup rather than running in a
state where a trust claim would be false, a secret would be publicly known, or a link mailed to a real user
would be unusable:
| Condition | Why it refuses |
|---|---|
SESSION_SECRET at its dev default |
Every member session cookie would be signed with a key published in this repository. |
ADMIN_SESSION_SECRET at its dev default |
Same, for admin-console sessions. |
POOL_SAMPLING_HMAC_SECRET at its dev default |
Human-audit sampling would be computable by anyone reading this repository, so a contributor could predict which submissions are never reviewed. |
PROFILE_SOURCE_SECRET at its dev default |
Credential-connect tokens forgeable and stored provider tokens decryptable. |
ADMIN_URL unset |
Would default to localhost:3002 and misroute every admin-console request to the wrong session cookie. |
APP_URL unset |
Every verification, password-reset and admin-invite email would link to http://localhost:3010. The account is unrecoverable and there is no server-side symptom, because sending succeeds. |
LANDING_URL unset |
Waitlist and public-profile links in outbound email would point at http://localhost:3001. |
EXECUTION_SANDBOX_ALLOW_EGRESS=true |
Contributor code would run with unrestricted network egress. |
EXECUTION_SANDBOX_VERIFY_ISOLATION=false |
The sandbox's applied isolation would never be read back, so every "execution verified" verdict would be unfounded. |
E2B_API_KEY is set but the E2B SDK or one of its dependencies cannot load |
The process would otherwise look healthy at startup, but every queued execution would fail later. Startup instead refuses to boot. |
One further guard is environment-independent: the execution timeouts must be ordered
inner worst case < command timeout < sandbox lifetime, or startup fails.
Mail is also fail-closed in production. With SMTP_HOST/SMTP_USER unset, a development build falls
back to a stream transport that discards the message; in production that fallback throws instead. Silently
discarding a verification code or a password reset while returning success to the caller is not an
acceptable default.
| Variable | Default | Notes |
|---|---|---|
PORT |
4000 |
|
HOST ✱ |
0.0.0.0 |
|
NODE_ENV |
— | Anything other than development/test enables the boot guards above — not just production/prod/live. |
LOG_LEVEL ✱ |
info |
|
TRUST_PROXY ✱ |
unset | Set when behind a load balancer, or client IPs in rate limits and audit logs will all be the proxy's. |
BODY_LIMIT_BYTES ✱ |
10485760 (10 MB) |
Request body cap. Large files use the upload flow, not request bodies. |
CORS_ORIGINS |
http://localhost:3000,http://localhost:3001,http://localhost:3002 |
Comma-separated. Must list every frontend origin you actually run, on the exact port. Miss one and the browser blocks its API calls with no server-side error — the page just sits there empty. This is the single most common "it's broken" report. |
PUBLIC_API_BASE_URL |
http://localhost:<PORT> |
Public origin of this API (no /v1). Builds the digest-unsubscribe link mailed to users and MCP artifact-upload URLs. Refused at boot in production while still the localhost default. |
MCP_PUBLIC_URL |
falls back to APP_URL, then http://localhost:3010 |
Public dashboard/MCP gateway origin (no path — /mcp is appended; a value ending in /mcp is normalised). Bound into every MCP OAuth credential as its audience and used for all advertised discovery/OAuth URLs. Refused at boot in production while still a localhost default. |
APP_URL |
http://localhost:3010 |
Dashboard origin. Used to build links in emails and notifications. |
LANDING_URL |
http://localhost:3001 |
The public landing app. Owns /domains/:id, /pools and public profiles — the waitlist "your domain is live" email links here, not to APP_URL, which has no /domains route. Refused at boot in production if unset. |
ADMIN_URL |
http://localhost:3002 |
Admin console origin. Required in production (above). |
COOKIE_DOMAIN ✱ |
unset | Cookie Domain attribute. Unset ⇒ host-only cookies, correct when every app is on one host. Set it to the shared parent domain (e.g. .databounty.io) when the dashboard, admin console and API are on different subdomains, or sessions will not be sent cross-subdomain. Read in lib/session-cookie.ts. |
COOKIE_SAMESITE ✱ |
lax |
Cookie SameSite attribute. Leave at lax unless a cross-site flow genuinely needs none — and none requires HTTPS. Read in lib/session-cookie.ts. |
MCP_ALLOWED_ORIGINS ✱ |
https://chatgpt.com,https://chat.openai.com,https://claude.ai |
Extra origins accepted on the MCP endpoint, beyond those derived from MCP_PUBLIC_URL/APP_URL. Comma-separated; setting the variable replaces this default list rather than adding to it. |
DB_POOL_MAX ✱ |
driver default | Max Postgres connections in the runtime pool (lib/prisma.ts). Lower it behind a session pooler, which multiplies connections per instance. |
DB_STATEMENT_TIMEOUT_MS ✱ |
unset | Server-side statement timeout applied to runtime queries (lib/prisma.ts). |
| Variable | Default | Notes |
|---|---|---|
SESSION_SECRET |
dev fallback | See the warning above. |
ADMIN_SESSION_SECRET |
dev fallback | Separate from the member secret on purpose — an admin session must not be derivable from a member one. |
GOOGLE_CLIENT_ID ✱ |
unset | Enables "Continue with Google". Email/password signup works without it. |
GOOGLE_TOKENINFO_URL ✱ |
https://oauth2.googleapis.com/tokeninfo |
Google token-introspection endpoint. Override only to point at a test double. |
GOOGLE_AUTH_TIMEOUT_MS ✱ |
5000 |
Timeout for that introspection call. |
Email is required for a usable deployment: mutations are gated on a verified email address, and the verification link is delivered by email. With SMTP unset, a new member can sign up but cannot verify, and therefore cannot claim a handle or submit anything.
| Variable | Default | Notes |
|---|---|---|
SMTP_HOST |
unset | Unset ⇒ nothing sends. |
SMTP_PORT |
unset | |
SMTP_SECURE |
unset (derived) | Implicit TLS on connect (SMTPS). Blank derives it from the port — true only for 465. Set true/1 or false to state it explicitly. Read at config.ts:227-229. |
SMTP_USER |
unset | |
SMTP_PASS |
unset | |
EMAIL_FROM |
noreply@databounty.io |
Change this. With most providers a sender you do not control will be rejected or spam-filed. |
| Variable | Default | Notes |
|---|---|---|
STORAGE_DRIVER ✱ |
unset (⇒ local) |
s3 for S3-compatible storage; anything else, including unset, selects the local-directory driver. Only local and s3 exist — any other value throws at first use. |
STORAGE_LOCAL_DIR ✱ |
unset | Local driver only. Not suitable for more than one API instance — the second instance cannot see the first's files. |
STORAGE_BUCKET ✱ |
unset | |
STORAGE_REGION ✱ |
unset | |
STORAGE_ENDPOINT ✱ |
unset | Set for S3-compatible services that are not AWS (MinIO, R2, Spaces). |
AWS_ACCESS_KEY_ID ✱ |
unset | |
AWS_SECRET_ACCESS_KEY ✱ |
unset | |
AWS_SESSION_TOKEN ✱ |
unset | Temporary credentials only. |
STORAGE_MAX_UPLOAD_BYTES ✱ |
104857600 (100 MB) |
Single-request upload cap. |
STORAGE_MULTIPART_THRESHOLD_BYTES ✱ |
104857600 (100 MB) |
Above this, uploads go multipart. |
STORAGE_MULTIPART_PART_SIZE_BYTES ✱ |
16777216 (16 MB) |
|
STORAGE_MAX_MULTIPART_UPLOAD_BYTES ✱ |
5368709120 (5 GB) |
|
STORAGE_REQUEST_TIMEOUT_MS ✱ |
15000 |
|
STORAGE_DIRECT_UPLOAD_EXPIRES_SECONDS ✱ |
900 (15 min) |
Lifetime of a browser-upload target issued by /v1/artifacts/upload-slot. Shorter is safer; too short and a slow connection cannot finish a large part. |
STORAGE_MAX_PENDING_UPLOADS_PER_USER ✱ |
20 |
Cap on simultaneously pending (created but not completed) artifacts per member. Stops one account from reserving unbounded upload slots. |
| Variable | Default | Notes |
|---|---|---|
ARTIFACT_SCAN_URL ✱ |
unset | Scanner endpoint. |
ARTIFACT_SCAN_TOKEN ✱ |
unset | |
ARTIFACT_SCAN_TIMEOUT_MS ✱ |
15000 |
Unset means uploads are not scanned. The scan status is recorded as not-performed rather than presented as clean — but you are accepting unscanned files from the internet, so configure it.
Scanning is additionally gated by the database setting artifacts.malware_scan.enabled (off by default), read
directly by src/services/artifact-scanner.ts. That key is not in the validated settings catalog below and
has no admin route or console UI, so it can currently only be enabled by writing the admin_settings row
directly. Documented gap.
The sandbox runs contributed code. Without a key, the execution stage records
no_provider_configured and routes the submission to human review — it never passes an item it could
not actually run.
| Variable | Default | Notes |
|---|---|---|
E2B_API_KEY |
"" |
Unset ⇒ execution stage fails closed as above. |
E2B_TEMPLATE |
"" |
Sandbox template id. |
EXECUTION_RUNNER_TIMEOUT_MS ✱ |
120000 |
|
EXECUTION_SANDBOX_ALLOW_EGRESS ✱ |
false |
true is refused in production. |
EXECUTION_SANDBOX_EGRESS_ALLOWLIST ✱ |
unset | Only meaningful with egress enabled. |
EXECUTION_SANDBOX_VERIFY_ISOLATION ✱ |
true |
false is refused in production. |
EXECUTION_SANDBOX_MAX_CPUS ✱ |
2 |
|
EXECUTION_SANDBOX_MAX_MEMORY_MB ✱ |
4096 |
|
EXECUTION_SANDBOX_MAX_FILE_SIZE_MB ✱ |
256 |
|
EXECUTION_SANDBOX_MAX_PROCESSES ✱ |
256 |
|
EXECUTION_SANDBOX_MAX_OPEN_FILES ✱ |
1024 |
|
EXECUTION_SANDBOX_ORDER ✱ |
e2b |
Comma-separated provider order tried per run (services/execution-providers/provider-order.ts). An unknown or duplicated name is rejected at boot rather than silently skipped. Only e2b is registered in this rebuild. |
| Variable | Default | Notes |
|---|---|---|
OPENROUTER_API_KEY |
unset | Unset ⇒ the LLM stage records outcome no_provider_configured with status: "pending_llm_review" in its evidence detail, and routes to human audit. It never auto-accepts. The stage is also off by default as a runtime setting, so the honest path is the default path. |
ANTHROPIC_API_KEY ✱ |
unset | Enables the anthropic provider in the multi-provider LLM layer (services/llm/config.ts, providerKey()). Unset ⇒ that provider is simply not selectable; it never degrades into a fabricated review. |
OPENAI_API_KEY ✱ |
unset | Enables the openai provider in the same layer, same terms. |
| Variable | Default | Notes |
|---|---|---|
GITHUB_OAUTH_CLIENT_ID |
unset | |
GITHUB_OAUTH_CLIENT_SECRET |
unset | |
ORCID_OAUTH_CLIENT_ID |
unset | |
ORCID_OAUTH_CLIENT_SECRET |
unset | |
ORCID_API_BASE_URL ✱ |
https://orcid.org |
Point at the sandbox host for testing. |
Unset providers render as unavailable in the UI rather than failing when clicked. LinkedIn, Google Scholar, Kaggle, X and Website are shown as unavailable by design — no verification is implemented.
| Variable | Default | Notes |
|---|---|---|
SLACK_CLIENT_ID |
unset | Slack needs an OAuth app. |
SLACK_CLIENT_SECRET |
unset | |
TELEGRAM_BOT_TOKEN |
unset | |
TELEGRAM_BOT_USERNAME |
unset |
Discord, Google Chat and Microsoft Teams are webhook-based — the member pastes a webhook URL, so they need no server configuration. An unconfigured channel dead-letters with the real reason and is never recorded as sent.
A published community dataset is pushed to every configured target. Both targets receive the same
data/items.jsonl and manifest.json bytes — so the two publications are verifiably the same
dataset — and differ only in the repository furniture each host reads. The public training split contains
contributor submissions only: sponsor reference examples remain review evidence and are never exported as
rows. If a legacy or concurrent close-out leaves more accepted submissions than the approved
Bounty.targetItems, publication deterministically exports only the oldest targetItems rows and records
an audit event for the cap.
| Variable | Default | Notes |
|---|---|---|
HUGGINGFACE_API_TOKEN |
unset | Write token. Without it the Hugging Face target records not_configured. |
HUGGINGFACE_NAMESPACE |
unset | User or org datasets are created under. |
HUGGINGFACE_API_URL |
https://huggingface.co/api |
Override for a self-hosted Hub. |
GITHUB_PUBLICATION_TOKEN |
unset | A dedicated credential, never the account token used by gh — contents:write scoped to just the one shared datasets repo below is enough. |
GITHUB_PUBLICATION_OWNER |
unset | Org or user that owns the shared datasets repo. |
GITHUB_PUBLICATION_REPO |
databounty-datasets |
The one shared repo every dataset publishes into, as its own folder (datasets/<slug>/) — not one repo per dataset. |
GITHUB_PUBLICATION_API_URL |
https://api.github.com |
Set for GitHub Enterprise. |
GITHUB_PUBLICATION_BRANCH_PROTECTED |
unset (false) |
When true, publish and unpublish push through a branch + pull request + merge instead of committing straight to main: create a branch from the built commit, open a PR (head: that branch, base: main), merge it via the API, then best-effort delete the branch. Default/unset keeps today's direct-push-to-main behavior unchanged. Turn this on only after the shared databounty-datasets repo actually has branch protection requiring PRs on main — setting this var does not itself enable that protection, request any GitHub permissions, or change the repo's settings; that is a separate owner action. |
PUBLICATION_REQUEST_TIMEOUT_MS |
10000 |
Shared by both targets. |
PUBLICATION_UPLOAD_TIMEOUT_MS |
300000 |
Per-file upload timeout. |
COMMUNITY_PUBLISH_PRIVATE |
unset (false) |
Off by default — production datasets are meant to be public. Set to true only in an environment whose GITHUB_PUBLICATION_REPO/HUGGINGFACE_NAMESPACE point at dev-only repos/orgs, so a test publish there never lands public. |
What lands in each repo
| File | Hugging Face | GitHub |
|---|---|---|
data/items.jsonl |
✅ | ✅ identical bytes |
manifest.json |
✅ | ✅ identical bytes |
data/contributor-items/… |
✅ | ✅ identical bytes |
README.md |
dataset card with the Hub's YAML front matter (license:, configs:, size_categories:) |
plain README — front matter would be meaningless noise on GitHub |
LICENSE |
— (the Hub reads the front-matter license: tag) |
✅ full verbatim licence text |
.gitattributes |
— | ✅ marks the JSONL generated, so it is collapsed in diffs and excluded from the language bar |
LICENSE carries the full text, not a pointer, because GitHub identifies a repository's licence by
matching that file against known texts — a repo naming its licence only in prose reads as all rights
reserved, the opposite of what an open dataset intends. Texts are bundled for CC-BY-4.0,
CC-BY-SA-4.0, CC0-1.0 and ODC-By-1.0 (the four a bounty can select). Any other licence value
means LICENSE is omitted rather than guessed, and the README says the full text is not bundled.
Contributor uploads are published. A role: "file" field holds an artifact id; the bytes behind it
are pushed to data/contributor-items/<submissionId>/<fieldKey>-<artifactId>-<filename>, and the item's
field value in items.jsonl is rewritten to that path, so an item and the file it names always
agree. Only ready artifacts of kind submission_attachment are eligible.
One publish run buffers each file in memory before pushing, so a cumulative 200 MB cap bounds the
job. The cap is applied to the artifact's verified stored size before a single byte is read. Going
over it is deliberately not fatal — a partial dataset with an accurate manifest beats blocking the
whole publish — but it is never silent: manifest.json carries
attachments: { resolved, published }, both READMEs print a Partial upload set notice, and an
audit-log row (community_publication.attachments_truncated) records the counts and the cap. An item
whose file was skipped keeps its raw artifact id rather than a path to a file that is not there.
A GitHub-specific limit. Git LFS is out of scope, so a single file at or over GitHub's 100 MB API limit fails that publish permanently, naming the file, rather than being dropped from the commit. The 200 MB cumulative cap does not prevent this — one 120 MB upload is under the cap but over GitHub's per-file ceiling, and would publish to Hugging Face and fail on GitHub. Hugging Face stays the primary target for datasets with large files.
Withdrawal (retract) never deletes. The admin retract action enqueues a community.unpublish job
that calls each provider's own withdrawal: Hugging Face and GitHub are both flipped to private, so
the commits and the contributor provenance in them survive, only public reachability is removed, and the
retraction stays reversible. A 404 counts as success — the dataset is already unreachable, which is the
state the call exists to guarantee.
A target is recorded retracted only after its provider confirms. If the withdrawal cannot be
performed — no credential, no recorded repo id, a provider error — the row stays published, because
the dataset really is still public, and the reason is written to lastError and to the audit log
(community_publication.retract_failed). The bounty-level status becomes retracted only when every
previously-published target actually withdrew; a partial withdrawal never displays as a complete one.
Retract is not gated on the community.publish.enabled kill switch — turning publishing off must
never prevent taking something down.
Per-instance, in-memory counters unless noted. Behind more than one API instance the effective limit is
the value below multiplied by the instance count — size them accordingly, and set TRUST_PROXY or every
request will share one bucket keyed on the load balancer's address.
| Variable | Default | Notes |
|---|---|---|
RATELIMIT_GLOBAL_MAX ✱ |
plugin default | Global per-IP request ceiling, read in app.ts. |
MCP_RATE_LIMIT_PER_MIN ✱ |
300 |
Per-credential MCP calls per minute. Backed by the api_key_rate_buckets table, so this one is shared across instances (lib/mcp-rate-limit.ts). |
API_KEY_RATE_LIMIT_PER_MIN ✱ |
300 |
Fallback for the above when MCP_RATE_LIMIT_PER_MIN is unset. |
MCP_TOKEN_RATE_LIMIT_PER_MIN ✱ |
60 |
Token requests per minute per client_id on POST /mcp/oauth/token (lib/mcp-rate-limit.ts). That endpoint carries no Authorization header — it is a public PKCE client — so the per-credential limiter in app.ts never sees it, and the per-IP fallback is useless behind a proxy with TRUST_PROXY unset. 0 disables. |
MCP_TOKEN_REPLAY_LIMIT_PER_MIN ✱ |
5 |
Token requests per minute keyed on the presented authorization code or refresh token, hashed. Rotation gives a healthy client a new secret each time, so it never nears this; a client replaying one dead token is throttled within seconds. Refused if either this or the per-client_id bucket is exceeded, answered 429 with an RFC 8628 slow_down body. 0 disables. |
RATE_BUCKET_PRUNE_INTERVAL_MS ✱ |
300000 |
How often expired one-minute rate-limit buckets are deleted (pruneOldRateBuckets, 10-window grace). Previously never scheduled at all, so the table grew unbounded. |
MCP_SESSION_SWEEP_INTERVAL_MS ✱ |
60000 |
How often idle sessionful-MCP transports are closed when no request is arriving (routes/mcp.ts). Expiry is also checked on each request; this timer is what reaps a client that went silent. 0 disables the timer and restores request-driven-only sweeping. |
MCP_MAX_SESSIONS_PER_CREDENTIAL ✱ |
8 |
Concurrent sessionful-MCP sessions one API key or OAuth grant may hold. Oldest are closed first when a new handshake arrives. A client that loses its session id mints a new session per request, so this is the bound on that. |
MCP_MAX_SESSIONS_TOTAL ✱ |
500 |
Process-wide ceiling on live MCP sessions, evicted oldest-first. Backstop for many credentials each under the per-credential cap. |
Read in worker.ts (and services/worker-heartbeat.ts, services/notifications.ts) rather than
config.ts. All are milliseconds, all have working defaults, and none needs setting for a normal
deployment — they exist to slow a sweep down on a small instance or speed one up while debugging.
Every value is a tick interval, not a deadline: shortening one does not make the underlying window
shorter, it only checks more often.
| Variable | Default | Sweeps |
|---|---|---|
WORKER_POLL_INTERVAL_MS ✱ |
2000 |
Job-queue claim loop. |
WORKER_TICK_TIMEOUT_MS ✱ |
— | Per-tick watchdog budget; a tick exceeding it is reported unhealthy (services/worker-heartbeat.ts). |
WATCHDOG_INTERVAL_MS ✱ |
60000 |
Stuck-submission detection. |
NOTIFICATIONS_DISPATCH_INTERVAL_MS ✱ |
15000 |
Outbox dispatch. |
NOTIFICATIONS_DISPATCH_LIMIT ✱ |
250 |
Rows claimed per dispatch pass (services/notifications.ts). |
NOTIFICATIONS_DISPATCH_CONCURRENCY ✱ |
16 |
Parallel sends per pass (services/notifications.ts). |
NOTIFICATIONS_DIGEST_INTERVAL_MS ✱ |
300000 |
Digest flush. |
AUDIT_REAPER_INTERVAL_MS ✱ |
300000 |
Returns expired audit claims to the queue. |
KARMA_HOLD_RELEASE_INTERVAL_MS ✱ |
300000 |
Releases karma past its hold window. |
POOL_RECONCILE_INTERVAL_MS |
600000 |
Recounts open-pool capacity and settles pools closed after reaching their verified-item target. Community pools are open-ended; time does not close them. |
ARTIFACT_SWEEP_INTERVAL_MS ✱ |
300000 |
Expires abandoned pending uploads. |
AGENT_ISSUE_SWEEP_INTERVAL_MS ✱ |
300000 |
Agent-reported issue housekeeping. |
LEADERBOARD_SWEEP_INTERVAL_MS ✱ |
600000 |
Leaderboard rank/movement snapshot. |
WAITLIST_SWEEP_INTERVAL_MS ✱ |
600000 |
Domain-waitlist notification sweep. |
PROFILE_SOURCE_SWEEP_INTERVAL_MS ✱ |
600000 |
Expires stale credential-connect state tokens. |
MCP_OAUTH_CLEANUP_INTERVAL_MS ✱ |
300000 |
Deletes expired MCP OAuth grants/codes. |
ADMIN_METRICS_SNAPSHOT_INTERVAL_MS ✱ |
300000 |
Admin overview metric snapshot. |
Read in lib/public-read-cache.ts. The two unauthenticated read routes the landing site renders from —
GET /v1/community/catalog and GET /v1/community/stats — are cached in-process, per validated query,
for this many milliseconds. The cached body never depends on the caller (no headers, cookies or session
go into the key), errors are never cached, and concurrent misses on one key share one computation.
Added 2026-09-22 after those two routes were measured as 85%+ of a steady ~10 GB/day of Supabase egress on
both deployments, driven by the landing container's own health probe re-rendering the homepage every
15 seconds rather than by visitors.
| Variable | Default | Effect |
|---|---|---|
CACHE_DRIVER ✱ |
noop |
Which cache driver backs lib/cache/: noop, memory or redis. An invalid value throws at boot rather than silently leaving the cache off. noop means the application behaves exactly as it did before the layer existed — every read a miss, every write a no-op — which is what makes enabling it safe. redis builds an in-process L1 in front of Redis; with REDIS_URL empty it logs and falls back to noop. |
REDIS_URL ✱ |
(empty) | Connection string used when CACHE_DRIVER=redis. On the dev deployment this is redis://redis:6379 — an internal Docker hostname, not published, so no credentials. The driver fails open behind a circuit breaker: if Redis is unreachable the app degrades to the database path instead of waiting out timeouts. Every key this application writes is namespaced databounty:, so the instance may safely be shared with another application — as it is in production, where the Redis also serves a co-tenant on the same host. What is cached: public catalogue reads (invalidated explicitly on write), verified API keys (60s, dropped immediately on revoke, rotation and account suspension — a cached credential never outlives the decision that revoked it), and the per-credential request rate-limit counters. The token-endpoint rate limits deliberately stay in Postgres whatever this is set to: they are a security control, and a shared Redis can evict a key under memory pressure. If the cache is off or unreachable, every counter falls back to Postgres and the limits keep applying — they never fail open. |
PUBLIC_READ_CACHE_TTL_MS ✱ |
60000 (0 under NODE_ENV=test) |
Maximum age of a cached public catalog/stats body. 0 bypasses the cache; the integration suite relies on that so a write is visible on the next read. Nothing downstream expects fresher than this — the landing refreshes itself every two minutes. |
| Variable | Default | Notes |
|---|---|---|
DATABOUNTY_API_KEY ✱ |
unset | Read by the stdio MCP entrypoint (src/mcp/stdio.ts) to authenticate as a member. This is a client-side variable for whoever runs the stdio server — it is not part of the API server's own configuration, and setting it on the API process does nothing. |
The three Next.js apps read only public values. Anything named NEXT_PUBLIC_* is compiled into the
browser bundle — never put a secret in one.
| Variable | Needed by | Notes |
|---|---|---|
NEXT_PUBLIC_API_URL |
web, admin, landing | Where apps/api is. |
NEXT_PUBLIC_DASHBOARD_URL |
web, admin, landing | Cross-links, e.g. landing's "sign in". |
NEXT_PUBLIC_ADMIN_URL |
web, admin | |
NEXT_PUBLIC_LANDING_URL |
web, admin, landing | Also the host in public-profile URLs. |
NEXT_OUTPUT |
any | standalone for a self-contained server bundle; export (web and admin only) for a static out/. |
Every origin you set here must also appear in the API's CORS_ORIGINS.
29 settings, each with a schema that rejects invalid values instead of accepting them silently, and a
full change history. Edited in the admin console at /settings, read live — no redeploy.
Values are read on demand, so a change takes effect on the next check. Anything already in flight finishes under the value it started with.
| Key | What it controls |
|---|---|
community.dispute_window_hours |
How long a contributor has to dispute a rejection before it is final. |
community.karma_holds.enabled |
Whether accepted-item karma is held through the dispute window instead of released immediately. |
community.human_audit_window_size |
Items per human-audit window — the batch a validator claims. Bounded 50–100. |
community.human_audit_failure_threshold_pct |
Percent of a window's selected items that must be rejected for the whole window to fail. |
community.min_karma_to_request |
Minimum karma before a member may file a dataset request. Your anti-spam dial. |
community.leaderboard.page_size |
Default leaderboard page size. |
community.leaderboard.max_page_size |
Hard ceiling on one leaderboard request. |
| Key | What it controls |
|---|---|
validation.dedupe.reject_threshold |
Duplicate score at or above which duplicate_check rejects. |
validation.llm.enabled |
The LLM review stage platform-wide. Off by default — deterministic checks first. |
submissions.max_revisions |
Revision attempts before final rejection. |
| Key | What it controls |
|---|---|
karma.tiers |
The tier ladder — id, label, karma threshold, accent colour, perks, early-access hours. |
karma.rules |
Flat award rates: per accepted item by difficulty, per audited item, and so on. |
karma.matrix |
Per-dataset-type karma rate inputs, keyed on registry id. |
reputation.score.base |
Starting reputation score. |
reputation.score.per_verified_credential |
Points per verified credential, up to a 100-point cap. |
The karma tiers shipped in code are
0 / 5 / 50 / 500. A deployment that has writtenkarma.tiersuses its stored value, which may differ by orders of magnitude. The code values are defaults, not the tiers — check the console before quoting a threshold to anyone.
| Key | What it controls |
|---|---|
notifications.digest.enabled |
Whether the periodic digest is sent at all. |
notifications.digest.time |
Local HH:mm the daily digest is flushed. |
notifications.digest.timezone |
IANA zone the time above is read in. |
notifications.delivery.max_attempts |
Retries before a delivery is parked in the dead-letter state. |
notifications.delivery.lease_seconds |
How long a dispatch worker holds an exclusive lease. |
notifications.delivery.backoff_seconds |
Retry backoff ladder; the last entry repeats. |
| Key | What it controls |
|---|---|
launch.community.enabled |
Master switch for community karma activity. |
launch.community_requests.enabled |
Authenticated dataset-request submission. |
launch.dashboard_submissions.enabled |
Submitting items from the dashboard UI. |
launch.api_submissions.enabled |
Submitting items via a direct API-key call. |
launch.public_profiles.enabled |
Anonymous public-profile reads. Disabled returns the same not-found response as a missing profile. |
| Key | What it controls |
|---|---|
auth.email_verification.ttl_hours |
How long a verification link stays valid. |
ratelimit.global.max |
Requests per window before 429. |
ratelimit.global.window_seconds |
Window length. |
Some routes carry their own tighter limits on top of the global one — see api.md.