Rate Limits and Auth
Purpose and Scope
OpenWiki exposes two public, potentially expensive capabilities: generating repository wikis and chatting with indexed repository context. Rate limiting protects those capabilities from accidental loops, public abuse, and cost spikes while still allowing a hosted instance to be useful without requiring every visitor to sign in. The repository implements this as application-level reservation checks before work begins. A request is converted into a stable client key hash, checked against storage-backed counters, and either allowed or rejected with a typed error that contains a human-readable message, the active limit, the reset time, the retry delay, and the limiting scope.
Authentication has a narrower role. OpenWiki does not require route-level authentication for every public wiki read, but it does protect internal operational endpoints that refresh repositories and revalidate cached pages. Those endpoints call a shared authenticateOpenWikiRequest helper before parsing or executing the request body. In development, local requests can be accepted through eve's localDev auth helper. In deployed environments, the code uses Vercel OIDC and can optionally constrain accepted subjects to a configured Vercel team and project. This keeps scheduled or internal maintenance requests separate from public repository creation and chat traffic.
Storage configuration errors are handled separately from rate-limit and authorization failures. When storage is not configured, many user-facing flows would otherwise fail with an implementation-specific database exception. OpenWiki centralizes the recognizable missing-DATABASE_URL condition into a 503 JSON response with a clear setup message. That makes the failure mode actionable for local development and deployment validation: the app tells the operator that storage is unavailable rather than making a visitor debug a generic server error.
Sources: lib/chat-rate-limit.ts, lib/repository-generation-rate-limit.ts, agent/lib/route-auth.ts, app/lib/storage-error.ts
Relevant Source Files
lib/chat-rate-limit.tsdefines the chat-specific rate-limit error, default chat quotas, environment-variable parsing, storage reservation call, and user-facing retry messages.lib/repository-generation-rate-limit.tsdefines the repository-generation rate-limit error, default generation quotas, repository cooldown handling, environment-variable parsing, and reservation call.agent/lib/route-auth.tsbuilds the shared internal-route authentication policy from eve channel auth helpers, local development auth, Vercel OIDC, and optional Vercel subject restrictions.app/lib/storage-error.tsconverts the known missing storage configuration condition into a 503 JSON response that tells operators to setDATABASE_URL.app/api/internal/refresh-repositories/route.tsprotects repository refresh operations with internal authentication, validates optional refresh limits with Zod, calls the refresh engine, and requests home-page revalidation.app/api/internal/revalidate-repository/route.tsprotects explicit revalidation requests, validates home or repository payloads, revalidates repository routes, and revalidates each non-overview wiki page for the repository.
Rate-Limit Model
Chat and generation limits follow the same contract even though they use different storage reservation helpers and different default quotas. Each exported enforcement function first checks whether its feature is enabled. If the relevant enablement environment variable is unset or empty, the limit is enabled by default. If the value is present, only truthy strings such as 1, true, yes, and on keep enforcement enabled. This default-on posture is important for public deployments because an operator receives protection without having to discover and configure every quota variable before launch.
The chat limiter is tuned for repeated conversational usage. Its defaults are 40 messages per client per hour, 200 messages per client per day, and 600 messages globally per hour. enforceChatRateLimit reads those settings, hashes the client identity from the incoming Request, and reserves a chat attempt for the target repoFullName. If all configured chat quotas are set to zero, the function returns without reserving anything. Otherwise, it depends on reserveChatMessageAttempt from storage to decide whether the request can proceed.
The repository-generation limiter is tuned for more expensive indexing jobs and includes an additional repository cooldown. Its defaults are 10 generations per client per hour, 50 per client per day, 120 globally per hour, and a 10-minute cooldown per repository. enforceRepositoryGenerationRateLimit follows the same enablement and zero-config bypass pattern as chat, then calls reserveRepositoryGenerationAttempt with the client key hash, repoFullName, and computed configuration. The cooldown value is configured in minutes and converted to milliseconds before it reaches the storage reservation layer.
Both limiters throw typed errors rather than returning response objects. ChatRateLimitError and RepositoryGenerationRateLimitError carry the active limit, resetAt, retryAfterSeconds, and scope fields, so route handlers can translate them into consistent API responses while preserving enough detail for UI retry copy or headers. Each module also exports a stable machine-readable code: chat_rate_limited for chat and repository_generation_rate_limited for generation. The message text is scope-aware: global overloads describe the instance as busy, daily limits describe the user's daily quota, and short-term client limits describe recent activity.
Sources: lib/chat-rate-limit.ts, lib/repository-generation-rate-limit.ts
Internal Route Authentication
Internal maintenance routes share a single authentication boundary through authenticateOpenWikiRequest. The helper delegates to eve's routeAuth with an ordered list of auth functions. In development, the list includes localDev(), making local operational flows easier to test without reproducing deployed OIDC behavior. The list always includes vercelOidc(resolveVercelOidcOptions()), so production requests are expected to satisfy Vercel OIDC authentication rather than relying on a public bearer token implemented in each route.
resolveVercelOidcOptions makes deployment scoping optional but available. If OPENWIKI_WEB_PROJECT_NAME and OPENWIKI_WEB_TEAM_SLUG are both present, it restricts accepted Vercel subjects to the configured team slug, project name, and any environment. If either variable is missing, the OIDC helper is called without explicit subject options. This keeps the helper small while allowing stricter deployments to bind internal requests to the intended Vercel project identity.
The protected refresh endpoint shows how this boundary is used in practice. POST /api/internal/refresh-repositories authenticates before reading operational parameters. It accepts an optional JSON body and validates scanLimit, enqueueLimit, generatorEnqueueLimit, and retryCooldownHours against hard maximums. The route then passes the request and parsed limits into refreshRepositories. After the refresh operation, it attempts home-page revalidation and logs a non-fatal error if that secondary revalidation request fails, preserving the main refresh response.
The explicit revalidation endpoint is similarly protected but has a different payload contract. POST /api/internal/revalidate-repository requires JSON and accepts either { kind: "home" } or a repository payload containing owner, repo, repositoryId, and revisionId. Home revalidation only calls revalidatePath("/"). Repository revalidation also computes the repository href, revalidates the home page and repository landing route, loads the stored wiki, and revalidates each page route except the overview page. The response echoes the repository and revision identifiers when repository revalidation completes.
Sources: agent/lib/route-auth.ts, app/api/internal/refresh-repositories/route.ts, app/api/internal/revalidate-repository/route.ts
Compact Reference
| Area | Name | Default or contract | Notes |
|---|---|---|---|
| Chat limit | OPENWIKI_CHAT_RATE_LIMIT_ENABLED | enabled when unset | Disable only by setting a non-truthy value. |
| Chat limit | OPENWIKI_CHAT_RATE_LIMIT_CLIENT_HOURLY | 40 | Per-client hourly chat message quota. |
| Chat limit | OPENWIKI_CHAT_RATE_LIMIT_CLIENT_DAILY | 200 | Per-client daily chat message quota. |
| Chat limit | OPENWIKI_CHAT_RATE_LIMIT_GLOBAL_HOURLY | 600 | Instance-wide hourly chat quota. |
| Generation limit | OPENWIKI_GENERATION_RATE_LIMIT_ENABLED | enabled when unset | Controls repository generation reservations. |
| Generation limit | OPENWIKI_GENERATION_RATE_LIMIT_CLIENT_HOURLY | 10 | Per-client hourly wiki generation quota. |
| Generation limit | OPENWIKI_GENERATION_RATE_LIMIT_CLIENT_DAILY | 50 | Per-client daily wiki generation quota. |
| Generation limit | OPENWIKI_GENERATION_RATE_LIMIT_GLOBAL_HOURLY | 120 | Instance-wide hourly generation quota. |
| Generation limit | OPENWIKI_GENERATION_RATE_LIMIT_REPO_COOLDOWN_MINUTES | 10 | Cooldown between generation starts for the same repository. |
| Auth scope | OPENWIKI_WEB_PROJECT_NAME | optional | Used with OPENWIKI_WEB_TEAM_SLUG to restrict Vercel OIDC subjects. |
| Auth scope | OPENWIKI_WEB_TEAM_SLUG | optional | Used with OPENWIKI_WEB_PROJECT_NAME to restrict Vercel OIDC subjects. |
| Storage setup | DATABASE_URL | required by storage | Missing storage is converted to a 503 setup response by storageConfigurationErrorResponse. |
| Exported item | Source | Behavior |
|---|---|---|
enforceChatRateLimit(input) | lib/chat-rate-limit.ts | Reserves a chat attempt for repoFullName and throws ChatRateLimitError when blocked. |
enforceRepositoryGenerationRateLimit(input) | lib/repository-generation-rate-limit.ts | Reserves a generation attempt and throws RepositoryGenerationRateLimitError when blocked. |
authenticateOpenWikiRequest(request) | agent/lib/route-auth.ts | Applies eve route auth for local development and Vercel OIDC-protected internal routes. |
storageConfigurationErrorResponse(error) | app/lib/storage-error.ts | Returns a 503 JSON response for the known missing-DATABASE_URL storage error. |
POST /api/internal/refresh-repositories | app/api/internal/refresh-repositories/route.ts | Authenticates, validates optional refresh controls, runs repository refresh, and requests home revalidation. |
POST /api/internal/revalidate-repository | app/api/internal/revalidate-repository/route.ts | Authenticates, validates home or repository payloads, and revalidates affected Next.js paths. |
Failure Handling and Operator Guidance
When a rate limit blocks work, the limiting module constructs retry copy from retryAfterSeconds. Durations under 90 seconds are shown in seconds, durations under 90 minutes are rounded up to minutes, and longer waits are rounded up to hours. This formatting keeps API and UI messages readable without exposing implementation details about storage windows. Invalid quota environment variables do not crash the app; the parsers fall back to defaults when values are empty, negative, or not finite non-negative integers.
The most important operational distinction is that rate-limit failures, authentication failures, validation failures, and storage-configuration failures happen at different layers. Rate limits protect public expensive actions before work is queued or streamed. Internal auth protects maintenance routes before request bodies are trusted. Zod schemas protect maintenance handlers from malformed JSON and out-of-range operational controls. Storage setup handling identifies the known missing database condition and returns a setup-oriented 503 response. Keeping those concerns separate makes routes easier to reason about and makes failures easier to surface in product UI.
For deployment hardening, keep rate limits enabled unless the instance is private or protected by an upstream access-control layer. Tune chat limits independently from generation limits because chat is frequent and interactive while generation is expensive and job-oriented. Configure OPENWIKI_WEB_PROJECT_NAME and OPENWIKI_WEB_TEAM_SLUG for internal routes when deploying on Vercel so scheduled refresh and revalidation calls are bound to the expected project identity. Finally, verify DATABASE_URL before testing public repository flows; without storage, OpenWiki cannot persist repositories, jobs, revisions, rate-limit reservations, or chat state.
Sources: lib/chat-rate-limit.ts, lib/repository-generation-rate-limit.ts, agent/lib/route-auth.ts, app/lib/storage-error.ts, app/api/internal/refresh-repositories/route.ts, app/api/internal/revalidate-repository/route.ts
Next Steps
After configuring these controls, test the public flows that exercise them: start a repository generation, send repository chat messages, and invoke the internal refresh or revalidation routes from the environment that is expected to call them. If you are tuning limits, change one quota family at a time and confirm that blocked requests include the expected scope and retry delay. For broader context, read the Repository API, Chat API, Internal Maintenance API, Storage Setup, and Keeping Wikis Fresh pages.