Configuration
Environment Variables
Section titled “Environment Variables”All configuration is done through environment variables in the .env file. At least one provider must be configured (Gemini, Claude, Groq, Cerebras, or Ollama); the route returns 503 otherwise.
| Variable | Required | Description |
|---|---|---|
GEMINI_API_KEY | One of these | Google AI API key (Gemini 3.5 Flash Lite) |
CLAUDE_API_KEY | One of these | Anthropic API key (Claude Haiku 4.5). No free tier - every call is billed, see the note below. |
GROQ_API_KEY | One of these | Groq API key (GPT-OSS 120B). Free tier. |
CEREBRAS_API_KEY | One of these | Cerebras API key (Llama 3.3 70B). Free tier. |
OLLAMA_BASE_URL | One of these | Base URL of a local Ollama daemon (e.g. http://127.0.0.1:11434) |
OLLAMA_MODEL | Optional | Ollama model tag, defaults to llama3.2. Use any tag from ollama list. |
OLLAMA_API_KEY | Optional | Bearer token sent as Authorization: Bearer {key} on every Ollama request. Only needed if your Ollama is behind auth. |
Provider Priority
Section titled “Provider Priority”The LLM chain composes from whatever’s configured in env. Ordering is fixed - cloud providers run first, Ollama last:
- Gemini 3.5 Flash Lite via Google (
GEMINI_API_KEY) - Claude Haiku 4.5 via Anthropic (
CLAUDE_API_KEY) - cross-vendor fallback ahead of Groq, opt-in only (no free tier, see the note below) - GPT-OSS 120B via Groq (
GROQ_API_KEY) - free-tier cross-vendor fallback - Llama 3.3 70B via Cerebras (
CEREBRAS_API_KEY) - a second free-tier cross-vendor fallback, so a Groq outage doesn’t leave the chain down to Claude (paid) or Ollama (self-host only) alone - Ollama (
OLLAMA_BASE_URL), local, only if explicitly configured
Exactly one model per vendor. A second model on the same API key shares that key’s quota, so it adds latency without adding redundancy; crossing vendors is what makes the fallback meaningful.
If a provider fails (timeout, rate limit, malformed response), the system automatically tries the next one. Because each provider uses a separate credential, their quotas are completely independent. Self-hosters who want a fully offline scanner should set only OLLAMA_BASE_URL and leave the cloud keys unset.
Running Locally with Ollama
Section titled “Running Locally with Ollama”For privacy-first deployments where every byte of the resume stays on your machine:
# install ollama from https://ollama.com and pull a modelollama pull llama3.2
# in your .env (or as shell vars before pnpm dev):OLLAMA_BASE_URL=http://127.0.0.1:11434OLLAMA_MODEL=llama3.2
# leave GEMINI_API_KEY / CLAUDE_API_KEY / GROQ_API_KEY / CEREBRAS_API_KEY unset for offline-only modeThe Ollama path uses Ollama’s format: 'json' so the model returns strict JSON without prompt-engineering tricks. First scan is slow on commodity hardware (60-120s for llama3.2:3b on a typical laptop); subsequent scans of the same resume hit the in-memory result cache and return in <100ms. Bigger models produce noticeably better suggestions but take longer.
The /api/analyze response includes _provider: "ollama-{model}" so you can confirm requests are landing locally and not falling back to a cloud key you forgot to remove.
Behind a reverse proxy or auth gate
Section titled “Behind a reverse proxy or auth gate”Vanilla ollama serve on 127.0.0.1 has no authentication, which is fine for a local-only setup. If your Ollama lives behind a reverse proxy that requires a bearer token, or you’re pointing at a hosted Ollama-compatible endpoint (OpenWebUI, LiteLLM, OpenRouter’s Ollama-compatible routes, a Cloudflare-tunneled daemon with a service token, etc.), set OLLAMA_API_KEY and the request will include Authorization: Bearer {key} on every call:
# in your .envOLLAMA_BASE_URL=https://ollama.your-domain.tldOLLAMA_MODEL=llama3.2OLLAMA_API_KEY=sk-your-proxy-tokenThe header is only attached when the env var is non-empty, so leaving it unset keeps the request shape identical to the local-only setup. Empty or whitespace-only values are treated as not set so a stray OLLAMA_API_KEY= line in .env does not produce a malformed Authorization: Bearer header that the proxy would reject.
Authentication
Section titled “Authentication”How users sign in (or whether they sign in at all) is a separate choice from the LLM provider, and it’s also driven by environment variables. ATS Screener supports three modes, picked automatically:
- Anonymous: leave Firebase and LDAP unset. The scanner is open and history is local. This is the default.
- Firebase: set the
PUBLIC_FIREBASE_*variables for Google / email sign-in and synced history. - Active Directory: set
LDAP_URLfor on-premise AD sign-in.
See Authentication for the full comparison and the Active Directory guide for AD setup. The Firebase variables are listed below.
# self-host without firebase: leave every PUBLIC_FIREBASE_* var unset (the default).# self-host with firebase: set all six.PUBLIC_FIREBASE_API_KEY=...PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.comPUBLIC_FIREBASE_PROJECT_ID=your-projectPUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.comPUBLIC_FIREBASE_MESSAGING_SENDER_ID=1234567890PUBLIC_FIREBASE_APP_ID=1:1234567890:web:abcFree Tier Limits
Section titled “Free Tier Limits”| Provider | Model | RPM | RPD | TPM | Cost |
|---|---|---|---|---|---|
| Gemini 3.5 Flash Lite | 15 | 500 | 250K | Free | |
| Anthropic | Claude Haiku 4.5 | - | - | - | Paid, no free tier |
| Groq | GPT-OSS 120B | - | - | 8K | Free |
| Cerebras | Llama 3.3 70B | - | - | - | Free |
Groq’s free-tier ceiling for GPT-OSS 120B is lower than the old Llama 3.3 70B leg it
replaced (8K TPM vs. the previous 12K), so it throttles faster under sustained load -
this is what makes Claude a meaningfully more reliable (if paid) fallback ahead of it.
Cerebras’s own free-tier ceiling hasn’t been measured against real traffic yet - see
buildCerebrasProvider’s comment in providers.ts for the conservative starting
point its request budget is sized against in the meantime.
Google, Groq, and Cerebras block at their limits and never auto-charge - you cannot
accidentally incur costs on any of them. Claude is the one leg where usage translates
directly to a bill; see the caution above before setting CLAUDE_API_KEY.
For the latest limits, see the official documentation:
Rate Limiting
Section titled “Rate Limiting”Rate limiting is configured in src/routes/api/analyze/+server.ts:
const RATE_LIMIT = { maxPerMinute: 10, maxPerDay: 200};Adjust these values based on your expected traffic and API key limits.
Timeouts
Section titled “Timeouts”Each provider has its own timeout. Vercel Fluid Compute is enabled by default and allows up to 300 seconds on the Hobby plan:
// Google: 30s, Claude: 12s, Groq: 15s → worst case total: 57stimeoutMs: 30_000; // buildGoogleProvidertimeoutMs: 12_000; // buildClaudeProvidertimeoutMs: 15_000; // buildGroqProviderTwo constraints govern these numbers.
They must sum to less than the route’s maxDuration (60s), or the platform kills the
function before the last leg can run, silently turning a three-provider chain into a
shorter one. 30 + 12 + 15 leaves 3s of margin.
Each provider’s token budget must be reachable inside its own timeout. Measured throughput is 311 tok/s on Flash Lite, so a 6,144-token Google budget needs 19.8s. The Claude and Groq budgets (3,072 tokens each) are conservative, unverified starting points rather than measured figures - Claude has no free-tier ceiling to tune against the way Groq’s TPM limit does, and Groq’s own budget was carried over from a now-deprecated model. If a budget were raised above what its timeout allows, any response that ran to full length would be aborted mid-flight, wasting the call and the fallback behind it. A unit test enforces this.
Typical Flash Lite requests answer in 9-11s, well below its 30s ceiling. Output size tracks the fixed 6-platform schema rather than resume length, so a short resume and a maxed-out one produce within 5% of the same number of output tokens.
If every provider fails the route returns 503 and logs llm.all_providers_failed at
error level, and the client falls back to rule-based scoring.