Server, accounts and billing
Backup status and Study-file download receipts
The browser's storage mode and the account upload queue are separate evidence. The Study backup panel waits for the scoped queue to answer; a read failure is shown rather than represented as an empty queue. Pending and failed uploads remain unconfirmed. A completed acknowledgement shows the actual snapshot date and explains that subsequent edits need another backup. Scope-epoch changes invalidate panel state and late feedback. A downloaded recovery file is reported as a download started, without claiming independent restore or browser-save confirmation. No provider, entitlement, host or billing behavior changes.
This page describes the suite's server code: the route handlers under src/app/api, the workspace access modes, and the Supabase and Stripe integrations behind future account mode. Account mode, backups and billing are present in the code but disabled unless a deployment opts in explicitly.
Overview
Most of the suite runs in the browser. The server does five things:
- proxies a few public lookups (PubChem, ChEMBL and UniProt suggestions);
- runs literature search and the optional AI features;
- renders share images;
- answers the workspace access check;
- in account mode, handles sign-in, Study backups and the Stripe subscription.
The browser Workspace stores its records locally; see Storage and state. The headers and outbound-host rules are covered in Security and network. Operational setup is in docs/workspace-setup.md and Deployment and environment.
Server-only modules:
| Module | Role |
|---|---|
src/lib/workspace/server-config.ts |
workspaceServerConfig(env), isLocalDevelopmentRequest, sameOriginRequest |
src/lib/workspace/server.ts |
Supabase clients (accountClient, billingAdminClient), verifiedWorkspaceAccount, serverWorkspaceAccess, PRIVATE_HEADERS |
src/lib/workspace/billing.ts |
stripeClient, reconcileCustomer |
src/lib/workspace/billing-policy.ts |
Pure functions: chooseWorkspaceSubscription, workspaceSubscriptionState |
src/lib/workspace/checkout-operation.ts |
prepareCheckout, the leased, idempotent checkout procedure |
src/lib/workspace/backup-integrity.ts |
BACKUP_MAX_BYTES, BACKUP_ID, backupDigest (SHA-256) |
src/lib/workspace/request-body.ts |
boundedRequestText, which limits both Content-Length and the streamed body |
src/lib/workspace/capabilities.ts |
WorkspaceAccess, decideWorkspaceCapability, ensureWorkspaceCapability (shared with the client) |
server.ts and billing.ts import server-only, so a client component that imports them fails to build.
Access modes
workspaceServerConfig reads the environment on each call.
| Setting | Effect |
|---|---|
WORKSPACE_ACCESS_MODE unset or anything other than account |
Browser mode, the default. accountsEnabled is false, so accountClient() returns null and serverWorkspaceAccess() returns BROWSER_WORKSPACE_ACCESS (kind: "browser", scopeId: "browser"). The client may read, export and write local work. Backup and share are refused. |
WORKSPACE_ACCESS_MODE=account |
Account mode. Access depends on a verified Supabase user and a row in workspace_subscriptions. Adding provider credentials does not enable this mode on its own. |
WORKSPACE_DEVELOPMENT=1 |
Separate local development namespace. Honoured only when neither VERCEL nor WORKSPACE_SITE_URL is set, and only for requests to localhost, 127.0.0.1 or [::1] (isLocalDevelopmentRequest). /api/workspace/access then returns a development access with a 24-hour validity and no account. Backup is refused for this kind. |
In account mode, serverWorkspaceAccess() works as follows:
- It verifies the user with
client.auth.getUser(), which checks the user against Supabase rather than trusting the cookie alone. - It reads
statusandpaid_throughfromworkspace_subscriptionsusing the user's own client, so row-level security applies. - It returns
entitlement: "active"only when the status isactiveortrialingandpaid_throughis in the future. Otherwise it returns"expired"when a row exists and"none"when it does not. - It sets
offlineUntilto the earlier ofpaid_throughand seven days from now.
decideWorkspaceCapability lets an account read and export even after expiry. Writing and backup need an active entitlement. Backup also needs the client to be online. Sharing is always refused, because no permission model has been released.
On the client, src/lib/workspace/access-client.ts fetches /api/workspace/access with cache: "no-store". It keeps the result in sessionStorage under suite:workspace-access:v1. A cached value is loaded as offline until the server answers. A failed request never extends the recorded expiry.
Route handlers
All handlers below are in src/app/api. "Origin check" means the handler compares the Origin header with the expected origin (see the next section).
The single-provider literature handler reads its static response messages from content/apps/literature-route-copy.ts. Compound lookup reads route-only input/not-found messages from content/apps/compound-route-copy.ts and reuses compoundLookupCopy.unavailable; the client-consumed compound dictionary has no added keys. This copy extraction preserves wording, statuses and route behaviour, including dynamic validation and quota messages (MNT-03).
Public lookups
| Path | Method | Purpose and upstream | Validation | Caching |
|---|---|---|---|---|
/api/search/compound |
GET ?name= |
Calculator reagent lookup. Answers from the curated structures when the name maps to one. Otherwise POSTs the canonical name to PubChem PUG REST for SMILES, formula, InChIKey and title. | Non-empty, at most 120 characters, no control characters. Only typed application-owned payload refusals return 422; unknown network, JSON and runtime errors return a fixed 503 without diagnostics (SEC-08). Input 400 and upstream not-found 404 remain. | Upstream next: { revalidate: 86400 }; no response Cache-Control set |
/api/search/compounds |
GET ?q= |
Retained PubChem autocomplete endpoint; the shared typing hook makes no automatic calls (SEC-02) | 3 to 120 characters, no control characters | Upstream cache: "no-store"; every response private, no-store |
/api/search/molecule |
GET ?name= |
Exact ChEMBL record (www.ebi.ac.uk/chembl/api/data/molecule/<id>.json) for a name in the local catalogue. Checks that the returned id matches. |
Name must resolve through findCompound, at most 120 characters |
Upstream revalidate: 86400 |
/api/search/molecule-description |
GET ?source=&id= |
ChEMBL mechanism rows or a PubChem compound description for an exact identifier | Exactly two parameters; chembl with CHEMBL<n> or pubchem with a numeric CID; query string at most 128 characters |
In-process map with a 1-hour expiry and at most 128 entries; errors are not cached; public, max-age=3600, s-maxage=3600 |
/api/search/proteins |
GET ?q= |
UniProt REST search, four queries in parallel, ranked | 2 to 120 characters | Upstream revalidate: 86400; public, max-age=3600, s-maxage=86400 |
/api/share-image |
GET ?path= |
Share-card image for a known path (cardForPath, ogCard); no external call |
Unknown path returns 404 | public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800; nosniff |
Literature and AI
All literature routes run on the Node.js runtime and set maxDuration. Their responses are private, no-store. The literature code is in src/lib/literature/.
| Path | Method | Purpose and upstream | Notable checks |
|---|---|---|---|
/api/literature |
POST | Single-provider search: Europe PMC, or OpenAlex for semantic queries and OpenAlex ids | Requires a present exact Origin; body at most 12,000 characters; validateQuery; cursor at most 4,096 characters. The OpenAlex path has a 5-minute in-process cache (32 entries), a 1 s gap between semantic requests per process, and reserveSearch. |
/api/literature/search |
POST | Multi-lane search through executeSearch (src/lib/literature/search/) across Europe PMC and OpenAlex keyword and semantic lanes |
Requires a present exact Origin; body at most 40,000 characters; a session id pattern. In account mode, a verified user becomes the quota owner. When no provider succeeds, returns 429 if any provider was rate-limited or out of allowance (with Retry-After when known), otherwise 503. |
/api/literature/discover |
POST | References, citing works, related works or shared references from OpenAlex; the seed paper may first be resolved through Europe PMC | requestBody (origin, 16,000 characters); one paper, or 2 to 5 papers for shared references; page 1 to 100 |
/api/literature/fulltext |
POST | Europe PMC full-text XML for a paper with a PMCID | requestBody; locator validates the id, DOI, PMID, PMCID and OpenAlex id. In-process cache of 12 entries for 5 minutes. Source documents are limited to 4,000,000 bytes. |
/api/literature/extract |
GET, POST | GET reports whether AI extraction is available. POST sends an abstract or full-text section to the Anthropic Messages API for evidence extraction. | extractionOwner, model id pattern claude-…, reserveExtraction |
/api/literature/plan |
GET, POST | AI search-strategy suggestion (createSearchPlanHandlers) |
body.consent === true; only query and consent keys allowed; query at most 2,000 characters; reserveExtraction and reserveSearchAssist |
/api/study/summary |
POST | AI summary of selected Study records | body.consent === true; body at most 400,000 characters; validateStudyPacket (at most 200 sources); focus is one of overview, evidence or handover |
AI access is decided by extractionOwner in src/lib/literature/extraction-access.ts:
- Every non-GET request must have an
Originequal to theWORKSPACE_SITE_URLorigin, or to the request's own origin when that variable is unset. - Local development is allowed without an account. "Local" means
NODE_ENVis notproduction,VERCELandWORKSPACE_SITE_URLare unset, and the request is to a loopback host. - Anywhere else,
LITERATURE_AI_ENABLED=1andWORKSPACE_ACCESS_MODE=accountare both required, and the caller must hold theworkspace.writecapability. reserveExtractionenforces a per-process daily limit (LITERATURE_AI_DAILY_REQUESTS, default 25), at least 10 s between calls per owner, and one call in flight per owner.anthropicMessage(src/lib/literature/anthropic.ts) sends the API key only tohttps://api.anthropic.com/v1/messages. It usesredirect: "error"and an 80 s timeout, and it never returns provider error text to the caller.
OpenAlex usage is metered by src/lib/literature/search/quota.ts. Europe PMC keyword lanes in /api/literature/search have only a per-process burst guard of 30 requests per owner and 120 in total per minute. OpenAlex and AI search assistance need the following:
- Locally, in-process daily counters are used.
- On a hosted deployment, the Supabase RPC
reserve_literature_search_quotais used. It needsLITERATURE_SEARCH_SHARED_QUOTA=1,SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY. Without them, OpenAlex lanes are refused and Europe PMC remains available. Owners are stored only as SHA-256 hashes of account ids. All anonymous callers share oneanonymousrow.
Workspace and accounts
| Path | Method | Purpose | Notable checks |
|---|---|---|---|
/api/workspace/access |
GET | Returns the current WorkspaceAccess |
force-dynamic. Local development override as above. 503 if the subscription read fails. |
/api/workspace/auth |
POST | Sends a Supabase email sign-in link (signInWithOtp) with redirect <origin>/workspace/auth/callback |
Origin check; body at most 4,096 bytes; email pattern, at most 254 characters. 503 when accounts are not configured. |
/api/workspace/sign-out |
POST | auth.signOut({ scope: "local" }) |
Origin check; X-Workspace-Scope must match a signed-in user |
/api/workspace/backups |
GET | Lists the caller's backups, 100 per page, newest first | Verified user; X-Workspace-Scope; before cursor at most 160 characters, with timestamp and UUID patterns checked before it is embedded in the PostgREST or filter |
/api/workspace/backups |
POST | Uploads one Study backup | See "Backups" below |
/api/workspace/backups/download |
POST | Returns one backup's JSON after an integrity check | Origin check; verified user; X-Workspace-Scope; body at most 1,024 bytes with an id matching BACKUP_ID |
/api/workspace/billing/checkout |
POST | Creates or recovers a Stripe Checkout session | See "Stripe" below |
/api/workspace/billing/portal |
POST | Creates a Stripe customer-portal session with return URL <origin>/workspace |
Origin check; verified user; X-Workspace-Scope; customer row read with the user's client |
/api/workspace/billing/reconcile |
POST | Re-reads the caller's subscriptions from Stripe | Origin check; verified user; X-Workspace-Scope |
/api/workspace/billing/webhook |
POST | Stripe webhook | Account mode required before constructing Stripe or reconciling; private/no-store 503 in browser mode even when credentials exist (SEC-07). Stripe signature; body at most 1 MiB; Node.js runtime |
Every workspace response uses PRIVATE_HEADERS: Cache-Control: private, no-store, Vary: Cookie and Referrer-Policy: no-referrer.
Outside src/app/api, the sign-in callback src/app/(workspace)/workspace/auth/callback/route.ts exchanges the code parameter (shorter than 4,096 characters) for a session. It then redirects with 303 to <origin>/workspace#signed-in or #auth-failed. The account page /workspace (src/app/(workspace)/workspace/page.tsx) shows account controls only when account mode and the Supabase settings are present. It shows billing controls only when the Stripe key, price, webhook secret and service key are also set.
Literature POST handlers require a present exact Origin before body processing or provider calls (SEC-05). The shared requestBody guard covers discover and fulltext too. This does not authenticate nonbrowser callers; deployment-level per-client/provider rate limiting remains open.
Origin and scope checks
sameOriginRequest(request, expectedOrigin) returns true only when all three of these hold:
WORKSPACE_SITE_URLyields a valid origin. It must be HTTPS, or HTTP onlocalhostor127.0.0.1, with no credentials, query or fragment.- The
Originheader equals that origin exactly. - The request URL's origin equals it too.
It is used by auth, sign-out, backups (POST), backups/download, and the checkout, portal and reconcile billing routes. Without WORKSPACE_SITE_URL, all of them return 403.
The webhook has no origin check. In account mode it relies on stripe.webhooks.constructEvent with STRIPE_WEBHOOK_SECRET. When accounts are disabled it returns private/no-store 503 before constructing Stripe, verifying a signature or writing subscription state.
Account routes also require the X-Workspace-Scope header to equal account:<user id>, and return 409 otherwise. This stops a tab that still believes it belongs to one account from acting on another account after a sign-in change. The browser client sets the header from its current access scope (src/lib/workspace/backup-queue.ts, src/components/suite/workspace/backups.tsx).
Public literature routes recover the browser's exact loopback origin through
requestOrigin when running without VERCEL or WORKSPACE_SITE_URL. Next
normalises local request URLs to localhost even under next start; the helper
accepts only a canonically serialised Host from its fixed loopback list at the
same port and URL scheme. The Origin must match that recovered host exactly.
Nonlocal, malformed, different-port and forwarded-host substitutions remain
refused. AI and account enablement checks are unchanged (REL-701).
Supabase
accountClient() creates a @supabase/ssr server client with the publishable key and Next's cookies(). Its cookie options are:
httpOnly: true;secure, unlessWORKSPACE_SITE_URLis an HTTP origin;sameSite: "lax";path: "/".
accountClient() returns null unless account mode is on and SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY are set.
billingAdminClient() creates a @supabase/supabase-js client with SUPABASE_SERVICE_ROLE_KEY, without persisting or refreshing a session. Only the backup upload, the checkout, reconciliation and the literature quota RPC use it. User-facing reads go through the user's client, so row-level security applies to them.
The migrations in supabase/migrations/ are applied in filename order.
| Migration | Creates |
|---|---|
202610070001_workspace.sql |
See below |
202610070002_workspace_checkout.sql |
See below |
202610090001_literature_search_quota.sql |
See below |
202610070001_workspace.sql creates:
- Tables
workspace_customers(owner to Stripe customer),workspace_subscriptions(status,paid_through,observed_at) andworkspace_billing_events(processed event ids). Row-level security is on. Authenticated users can select only their own customer and subscription rows. Writes are reserved forservice_role. - The function
apply_workspace_subscription, which issecurity definerand executable only byservice_role. It skips an event id it has already processed. It writes a snapshot only if that snapshot is not older than the storedobserved_at. - The table
workspace_backups(byte_sizebetween 1 and 4,194,304), readable by its owner. - The private storage bucket
workspace-backups(4 MiB limit,application/jsononly), with a read policy limited to the owner's folder. There is no client upload, update or delete policy.
202610070002_workspace_checkout.sql creates:
- The table
workspace_checkout_operations, one row per owner, accessible only toservice_role. - The functions
claim_workspace_checkout(a 2-minute lease),rotate_workspace_checkoutandfinish_workspace_checkout, all executable only byservice_role.
202610090001_literature_search_quota.sql creates:
- The table
literature_search_quota, keyed by provider and owner key (global,anonymousor a 64-hex hash). It is accessible only toservice_role. - The function
reserve_literature_search_quota. It validates its arguments, takes an advisory transaction lock per provider, rolls daily counters over in UTC, and enforces the global and per-owner daily limits and a one-per-second gate for semantic queries.
docs/workspace-setup.md lists the release checks that must pass before these migrations are relied on. These include testing row-level security with two real accounts.
Stripe
stripeClient() returns a client only when STRIPE_SECRET_KEY is set. The client is configured with maxNetworkRetries: 2 and timeout: 15000.
Checkout
/api/workspace/billing/checkout works as follows.
It returns 503 unless the Stripe key, service key,
STRIPE_WORKSPACE_PRICE_ID,WORKSPACE_SITE_URLandSTRIPE_WEBHOOK_SECRETare all present.It retrieves the configured price and refuses it unless the price is active, in GBP, yearly with an interval count of 1, licensed and has a positive amount. The browser never supplies a price.
It maps the owner to a Stripe customer. A new customer is created with the idempotency key
workspace-customer-<owner>and stored with an upsert that ignores duplicates. The stored row is then re-read.It calls
prepareCheckout(src/lib/workspace/checkout-operation.ts). That function:- claims the owner's lease through
claim_workspace_checkout; - refuses if a Workspace subscription already exists in a live or recoverable state;
- recovers an existing session by its stored id, or by the operation id in its metadata;
- refuses an open session that predates the lease;
- rotates to a new operation only when the old session has expired, or completed into a cancelled subscription;
- creates a new session with the idempotency key
workspace-checkout-<operation id>.
An operation older than 23 hours (
CHECKOUT_RETRY_WINDOW_MS) is refused rather than recreated, because Stripe may prune idempotency keys after 24 hours. Ownership is checked against the session's customer and metadata. The success URL is<origin>/workspace#subscription-pending.- claims the owner's lease through
Returning from checkout does not grant access. Only reconciliation does.
Reconciliation
reconcileCustomer(customerId, eventId) in src/lib/workspace/billing.ts is called by the webhook and by /api/workspace/billing/reconcile. It works as follows.
- It looks up the owner. An unknown Stripe customer grants nothing.
- It skips event ids it has already processed.
- It lists the customer's subscriptions from Stripe. It reads the current state rather than trusting the event body, and stops at 1,000 subscriptions.
chooseWorkspaceSubscriptionpicks, among subscriptions with an item on an eligible price, the one with a current active or trialing status first, then the latest paid-through date. Itsstatusandpaid_through(the earliest period end among its eligible items) are written. If no subscription has an eligible price, the owner's previous subscription id is written with the statusunentitled. If no Workspace subscription was ever recorded, nothing is written.- It writes the result through
apply_workspace_subscription.
The reconcile route uses the event id reconcile:<user>:<minute>, so repeated presses within one minute record at most one reconciliation.
The webhook handles these event types:
customer.subscription.created,.updatedand.deleted;invoice.paid,.payment_failedand.payment_action_required;checkout.session.completedand.async_payment_succeeded.
It returns 503 when reconciliation fails, so Stripe retries the delivery.
Legacy prices
stripeEntitlementPriceIds is the current STRIPE_WORKSPACE_PRICE_ID plus the exact ids in the comma-separated STRIPE_WORKSPACE_LEGACY_PRICE_IDS. Legacy ids keep existing subscribers entitled during reconciliation and in the checkout's existing-subscription check. New checkouts always use the current price. docs/workspace-setup.md describes the order in which to change these settings.
Backups
A backup is an explicit snapshot of one Study, not synchronisation. POST /api/workspace/backups works as follows.
- It runs the origin check, verifies the user and checks
X-Workspace-Scope. It returns 503 if there is no service-role client. - The
Idempotency-Keyheader must be a UUID matchingBACKUP_ID. This key becomes the backup id. - It checks the
workspace.backupcapability withserverWorkspaceAccess(), which returns 402 when the caller is not entitled. It also checks that the account has not changed. - The body is read with
boundedRequestTextup toBACKUP_MAX_BYTES(4 MiB, below Vercel's request limit), parsed, and validated withvalidateStudyBundle(src/lib/workspace/bundle.ts). - It computes a SHA-256 digest and uploads to
workspace-backups/<user id>/<id>.jsonwithupsert: false. If a row with that id already exists, the route returns it when the owner and digest match, and 409 otherwise. If the upload fails because an earlier attempt already stored the object, the stored object's digest is compared before continuing. - It inserts the metadata row, re-reads it, and confirms the owner and digest before acknowledging.
The client queue in src/lib/workspace/backup-queue.ts retries with the same operation id and body.
A failed upload attempt retains its body, operation id and checksum. The panel says the backup has "not been confirmed": a lost response cannot establish whether the server stored it (REL-700). Reload and retry retain the same operation, and only the matching acknowledgement marks it backed up.
POST /api/workspace/backups/download reads the metadata row and the storage object with the user's client. It checks the stored size and the SHA-256 digest before returning the body with an X-Content-SHA256 header. Download has no entitlement check, so expired accounts can still recover their work.
Local recovery files, which never reach the server, are limited by STUDY_BUNDLE_LIMITS: 50 MiB, 10,000 records and the per-file limit ORIGINAL_FILE_MAX_BYTES.
What is disabled by default
With no environment variables set:
| Feature | Default behaviour |
|---|---|
| Accounts | Off. /api/workspace/access returns browser access. The auth, sign-out and other account mutation routes return 403 because WORKSPACE_SITE_URL is unset. |
| Backups, portal, reconcile | 403 from the origin check on POST routes; the backup list (GET) returns 401 with no verified user |
| Checkout | 403, or 503 until every Stripe and Supabase setting is present |
| Webhook | Private/no-store 503 unless account mode is enabled and both STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are set. Browser mode returns before Stripe construction or reconciliation (SEC-07). |
| AI extraction, search plan, Study summary | 403 on hosted deployments unless LITERATURE_AI_ENABLED=1 and account mode are set; 503 without ANTHROPIC_API_KEY. Local development can use a key without an account. |
| OpenAlex search lanes | Refused on hosted deployments without the shared quota settings. Europe PMC search works. |
| Public lookups and share images | Always on |
Server environment variables
Names only. Never put values in documentation or source control. docs/workspace-setup.md covers provider setup.
| Variable | Read in | Required when |
|---|---|---|
WORKSPACE_ACCESS_MODE |
server-config.ts, extraction-access.ts, search/quota.ts, api/literature/search/route.ts |
Set to account to enable accounts |
WORKSPACE_SITE_URL |
server-config.ts, extraction-access.ts, literature/sources.ts, search/quota.ts |
Any account mutation, sign-in redirect or checkout. Its presence also marks a deployment as hosted. |
WORKSPACE_DEVELOPMENT |
server-config.ts |
Optional local development namespace only |
SUPABASE_URL |
server-config.ts |
Account mode; the shared literature quota |
SUPABASE_PUBLISHABLE_KEY |
server-config.ts |
Account mode |
SUPABASE_SERVICE_ROLE_KEY |
server-config.ts |
Backups, checkout, reconciliation, the shared literature quota. Server only. |
STRIPE_SECRET_KEY |
server-config.ts |
Billing |
STRIPE_WEBHOOK_SECRET |
server-config.ts |
Webhook; checkout also requires it |
STRIPE_WORKSPACE_PRICE_ID |
server-config.ts |
Checkout and reconciliation |
STRIPE_WORKSPACE_LEGACY_PRICE_IDS |
server-config.ts |
When previous prices still have subscribers |
ANTHROPIC_API_KEY |
literature/anthropic.ts, the extract and summary routes, search/assistant.ts |
AI features |
ANTHROPIC_LITERATURE_MODEL |
The extract and summary routes, search/assistant.ts |
Optional; defaults to EXTRACTION_MODEL in src/lib/literature/extraction.ts |
ANTHROPIC_WORKSPACE_ID |
literature/anthropic.ts, search/assistant.ts |
When the API key requires a workspace |
LITERATURE_AI_ENABLED |
extraction-access.ts, search/quota.ts |
Set to 1 for hosted AI features |
LITERATURE_AI_DAILY_REQUESTS |
extraction-access.ts |
Optional; 1 to 1,000, default 25 |
OPENALEX_API_KEY |
api/literature/route.ts, literature/sources.ts, search/providers.ts |
Optional; a larger OpenAlex allowance |
LITERATURE_SEARCH_SHARED_QUOTA |
search/quota.ts, search/assistant.ts |
Set to 1 for hosted OpenAlex lanes and AI search assistance |
LITERATURE_OPENALEX_DAILY_REQUESTS, LITERATURE_SEARCH_OWNER_DAILY_REQUESTS |
search/quota.ts |
Optional; defaults 500 and 100 |
LITERATURE_SEARCH_ASSIST_DAILY_REQUESTS, LITERATURE_SEARCH_ASSIST_OWNER_DAILY_REQUESTS |
search/quota.ts |
Optional; default 25 each |
VERCEL, NODE_ENV |
server-config.ts, extraction-access.ts, literature/sources.ts, search/quota.ts |
Set by the platform; used to tell local from hosted |
The build-time and public variables (NEXT_PUBLIC_SUITE_ORIGIN, SUITE_INDEXABLE and others) are covered in Deployment and environment.