Config
Projects
AI agents: fetch https://docs.erpc.cloud/config/projects.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /config/projects.llms.txt

Projects

A project is a tenant inside one eRPC deployment: your frontend, your indexer, and your test environment can each get their own endpoint, providers, auth rules, and rate-limit budget — served from a single process. Everything under this section configures what lives inside a project.

Agent reference

Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly:

Prompt Example #1: add a second project for my indexer
I want to create a second eRPC project in my eRPC config for my indexer traffic — separate
upstreams, longer timeouts, and a different rate-limit budget from my frontend project.
Walk me through setting up projects[].id, upstreams, networks, and auth. Also point me
at the child pages I should read for each section. Reference:
https://docs.erpc.cloud/config/projects.llms.txt
Prompt Example #2: understand project URL routing
Explain how eRPC routes requests to the right project based on the URL structure, and
how projects[].id maps to the endpoint path. I want to know the exact URL pattern and
any multi-tenant gotchas before I deploy. Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/projects.llms.txt
Projects — agent starting pointsExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

This page is an orientation hub. Implementation detail lives in the child pages — fetch their machine-readable companions:

Project-level config keys: projects[].id, projects[].auth, projects[].cors, projects[].networkDefaults, projects[].upstreamDefaults, projects[].rateLimitBudget, projects[].healthCheck, projects[].providers, projects[].upstreams, projects[].networks, projects[].allowClientDirectives. Each is documented exhaustively in the child page whose topic it belongs to; multi-tenant behavior (URL shape /{projectId}/{architecture}/{chainId}) is covered in URL structure.

How it works

Isolation boundaries. ProjectsRegistry is built once at startup and eagerly registers every projects[] entry. Per project: a health tracker (rolling-window scorer), a providers registry, an upstreams registry, an optional auth registry (only when auth is configured), a selection-policy JS runtime, and a networks registry. Shared across projects: the rate-limiters registry, shared-state registry, the EVM JSON-RPC cache (project-scoped at network-prepare time via evmJsonRpcCache.WithProjectId(projectId)), the vendors registry, and the proxy-pool registry. Duplicate project ids fail startup with ErrProjectAlreadyExists.

Request routing (HTTP). handleRequest first applies domain aliasing: each server.aliasing.rules[] entry is matched against the request Host (port stripped) with a wildcard; the first match pre-selects projectId/architecture/chainId. Then parseUrlPath fills remaining values from path segments. A trailing /healthcheck segment, or any non-POST/non-OPTIONS method (e.g. GET), flags a healthcheck rather than an RPC call. Project resolution via erpc.GetProject — unknown id returns ErrProjectNotFound → HTTP 404.

gRPC routing. The project comes from x-erpc-project metadata (required, else InvalidArgument); chain from x-erpc-chain-id; architecture from x-erpc-architecture, defaulting to "evm". The shared RequestProcessor then performs auth + forward identically to HTTP. For query streams, AcquireRateLimitPermit is called explicitly before the query executor because streams bypass Forward.

Per-request pipeline inside a project. Each entry of a (possibly batched) request body is processed in its own goroutine: payload validation → forwardHeaders capture (wildcard match against incoming header names; matched values forwarded to upstream HTTP requests) → ignoreMethods/allowMethods gate (allow overrides ignore; rejected methods return JSON-RPC error -32601 without touching auth or upstreams) → auth payload extraction from query/headers → project.AuthenticateConsumer (no-op returning nil user when project has no auth config) → network id resolution (from path or "networkId" field in body; lazy network bootstrap on first use) → directive defaults + HTTP enrichment → project.Forward.

PreparedProject.Forward. Resolves the network (lazy-loads on first request; lazily created network configs are exposed back into project.Config.Networks via ExposeNetworkConfig), sets it on the request before rate limiting so metrics have the network label, then AcquireRateLimitPermit enforces the project-level budget. After that the "requests received" counter fires (so project-rate-limited requests are not counted as received). For EVM a pre-forward hook may short-circuit entirely (e.g. eth_chainId served statically, cache-affecting normalizations for eth_blockNumber/eth_call/eth_getLogs/trace_filter), otherwise network.Forward plus a network post-forward hook runs. If the network has shadow upstreams the response is cloned and mirrored asynchronously.

Rate-limit layering. Project-level: projects[].rateLimitBudget names a budget in global rateLimiters.budgets[]; AcquireRateLimitPermit evaluates rules matching the method and calls TryAcquirePermit(... origin="project"); over-limit → ErrProjectRateLimitRuleExceeded → HTTP 429. Auth-level: each auth strategy may carry its own rateLimitBudget, overridable per-user (JWT claim / database record); checked inside Authenticate after a strategy succeeds, over-limit → ErrAuthRateLimitRuleExceeded → 429. Network- and upstream-level budgets are separate layers. All layers fail open: no store configured, Redis not yet connected, Redis timeout, or admission-semaphore saturation all allow the request.

CORS. Two CORS surfaces: admin (admin.cors, checked before admin request handling) and per-project (projects[].cors, checked right after project resolution). handleCORS: if the request carries no Origin header it is always allowed (no point enforcing CORS for non-browser clients — eRPC treats CORS as a soft deterrent, not a security boundary). With an Origin present, each allowedOrigins pattern is evaluated with a wildcard/boolean match. Allowed origin → echo the origin in Access-Control-Allow-Origin plus methods/headers/expose/credentials/max-age headers; OPTIONS preflight then terminates with 204. Disallowed origin → no CORS headers but the request still proceeds (browser blocks it; curl etc. succeed), except OPTIONS which gets a bare 204. CORS for a project only runs when cors is configured (nil → OPTIONS falls through to the JSON-RPC body parser).

Auth payload extraction precedence (HTTP). ?token= (deprecated, highest priority) → ?secret= → header X-ERPC-Secret-TokenAuthorization: Basic … (password half = secret; username ignored) → Authorization: Bearer … (JWT) → ?jwt=?signature=&message= (SIWE; message may be base64) → headers X-Siwe-Message + X-Siwe-Signature → fallback type network (client-IP based).

Consumer authentication. AuthRegistry.Authenticate: nil payload → 401; zero strategies → allow (nil user). Iterate strategies in order; skip non-applicable (method filter / unsupported payload type); first success attaches a User to the request and enforces the strategy's rate-limit budget. If every applicable strategy errors, errors are joined into one ErrAuthUnauthorized. Auth rate-limit denial still returns the authenticated user alongside the 429 error, so user labels remain populated on rate-limit metrics.

Admin RPCs. erpc_project(params: [projectId]){config, health}; erpc_taxonomy → all projects/networks/upstreams with aliases; API-key CRUD resolves the project's database-auth connector; cordon RPCs resolve upstreams through the project registry. Admin auth itself is global (admin.auth), not per-project; requests to an instance without admin config return ErrAuthUnauthorized "admin is not enabled for this project".

Healthcheck and projects. No project in path → all projects evaluated; a project with zero initialized upstreams lazily prepares upstreams for its static networks before evaluating; providers-only projects with no initialized upstreams report healthy with an explanatory message.

HTTP status code mapping. 400 invalid path/unmarshal/invalid request; 401 ErrAuthUnauthorized; 404 ErrProjectNotFound/ErrNetworkNotFound; 429 ErrAuthRateLimitRuleExceeded/ErrProjectRateLimitRuleExceeded/ErrNetworkRateLimitRuleExceeded/capacity; everything else 200 (JSON-RPC app errors). The top-level fatal-error writer forces 200 for POST per JSON-RPC convention.

Wildcard/boolean pattern grammar. Used by CORS origins, aliasing domains, forwardHeaders, ignoreMethods/allowMethods, and rate-limit rule methods. Tokens: | (OR), & (AND), ! (NOT), parentheses. Leaf patterns are glob matches where * = any sequence, ? = exactly one char, . = any single character (not a literal dot). Operator precedence: NOT > AND > OR. For values starting with 0x, prefixes >=, <=, >, <, = enable hex/decimal numeric comparison. No escape syntax exists.

Config schema

projects[] — ProjectConfig

Dotted pathTypeDefaultBehavior / interactions
projects[]*ProjectConfigIf empty/nil: a default project id: "main" is injected with server.aliasing.rules: [{matchDomain: "*", serveProject: "main"}] so /evm/123 works without a project segment. The injected project carries opinionated networkDefaults (getLogsMaxAllowedRange: 30000, getLogsSplitOnError: true, retry 5 attempts, timeout 120 s, hedge quantile 0.7 max 2) and upstreamDefaults (getLogsAutoSplittingRangeThreshold: 5000, retry 1 attempt delay 500 ms, timeout 60 s). Validation requires projects to be non-nil after defaulting.Config root
projects[].idstring— (required)Used as the project label on nearly every metric, in URL path routing, gRPC x-erpc-project, admin RPC params. Uniqueness enforced at registration: duplicate → ErrProjectAlreadyExists.
projects[].auth*AuthConfignil (no auth → all requests allowed; AuthenticateConsumer returns (nil, nil))When set, an auth registry is built at registration. Requires ≥1 strategy.
projects[].cors*CORSConfignil (no CORS handling at all; OPTIONS not short-circuited)Checked after project resolution.
projects[].providers[]*ProviderConfigIf both providers and upstreams are empty: two default providers are injected: {id: "public", vendor: "repository"} and {id: "envio", vendor: "envio"}.Unique ids required. Per-project providers registry.
projects[].upstreamDefaults*UpstreamConfignilApplied to every upstream via ApplyDefaults before that upstream's own SetDefaults; also passed to the providers registry.
projects[].upstreams[]*UpstreamConfignilShorthand non-http(s)/grpc endpoints (e.g. alchemy://KEY) are converted into providers and removed from the list at SetDefaults time. Requires ≥1 of upstreams/providers. Unique ids required.
projects[].networkDefaults*NetworkDefaultsnilApplied to statically defined networks at SetDefaults and to lazy-loaded networks at first request. Its own rateLimitBudget is network-level, distinct from project-level.
projects[].networks[]*NetworkConfignilNetworks not listed are lazily created on first request and the resulting config is appended back into Config.Networks (visible via admin erpc_project). Unique network ids and unique aliases required.
projects[].rateLimitBudgetstring"" (no project-level limiting)Names a budget id under global rateLimiters.budgets[]. Enforced per request in AcquireRateLimitPermit. Must exist in rateLimiters — unknown budget fails startup.
projects[].userAgentMode"simplified" | "raw""" → treated as simplified at request timesimplified buckets the User-Agent into ~20 low-cardinality names (curl, viem, ethers, chrome, …); raw stores it verbatim (high metric cardinality). Used for the agent_name metric label. Query param ?user-agent= takes precedence over the header.
projects[].forwardHeaders[]string (wildcard patterns)nil (forward nothing)Each pattern is wildcard-matched against every incoming header name; matches are forwarded to the upstream HTTP request. FOOTGUN: values are stored under the pattern key, so a wildcard pattern like X-Custom-* forwards the value under the literal header name X-Custom-*, not the original header name. Exact names work as expected.
projects[].allowClientDirectives*string (wildcard pattern)nil (all directives allowed)Controls which X-ERPC-* request directives (headers and query params) clients may use. The pattern is pre-compiled at project registration and evaluated against each directive's query-param key (e.g. skip-cache-read, use-upstream, skip-consensus) using the standard wildcard/boolean grammar. nil/omitted = all allowed (backward-compatible). "" = none allowed. "!skip-cache-read & !use-upstream" = all except those two. Config-set directiveDefaults always apply regardless of this filter. Does not filter X-ERPC-Force-Trace (which bypasses OTel sampling at span creation, before project resolution). Overridable per caller via projects[].auth.strategies[].allowClientDirectives.
projects[].auth.strategies[].allowClientDirectives*string (wildcard pattern)nil (inherit project value)Per-caller override of the project-level pattern, applied to users authenticated by this strategy. Same syntax and same "" = none / "*" = all semantics. Overrides in both directions: a strategy may widen (project "" + strategy "*") or narrow (project "*" + strategy "") — the project value is a default, not a ceiling. Typical shape is deny-by-default at the project and re-grant to trusted credentials. The capability is attached to the User by the authenticating strategy, so a caller identified only through trustUserIdHeader always falls back to the project value — an unvalidated identity header can never widen directive access.
projects[].ignoreMethods[]string (wildcard)nil (nothing ignored)If any pattern matches the JSON-RPC method, the method is rejected — unless re-allowed by allowMethods. Rejection is JSON-RPC error code: -32601 ("method not supported: X") returned without auth/upstream work.
projects[].allowMethods[]string (wildcard)nilOverrides ignoreMethods (e.g. ignoreMethods: ["*"] + allowMethods: ["eth_getLogs"] = only eth_getLogs). NOTE: allowMethods alone does NOT create an allowlist — a method matching neither list is still served (initial shouldHandleMethod = true).
projects[].scoreMetricsWindowSizeDuration0 → falls back to 1 minute at runtimeRolling window of the per-upstream health tracker (10 sliding buckets). FOOTGUN: source-code comments in two places say "10m" but the actual code value is var ScoreMetricsWindowSize = 1 * time.Minute. To get a 10-minute window you must set scoreMetricsWindowSize: 10m explicitly. See source (opens in a new tab).

projects[].cors.* / admin.cors.* — CORSConfig

Dotted pathTypeDefault (after SetDefaults)Behavior
cors.allowedOrigins[]string["*"] programmatically — but Validate() errors if the field is missing/empty for explicit cors: blocks: *.cors.allowedOrigins is required.Each entry is a full wildcard/boolean pattern matched against the Origin header value. Match errors are logged and that pattern skipped.
cors.allowedMethods[]string["GET","POST","OPTIONS"]Joined with ", " into Access-Control-Allow-Methods. Informational for the browser; no server-side enforcement.
cors.allowedHeaders[]string["content-type","authorization","x-erpc-secret-token"]Joined into Access-Control-Allow-Headers.
cors.exposedHeaders[]stringnil (no default)Joined into Access-Control-Expose-Headers. When nil (the common default), strings.Join(nil, ", ") returns "", so the server sends Access-Control-Expose-Headers: "". Browsers interpret this as zero extra headers exposed — any frontend JS call to response.headers.get('X-ERPC-Cache') returns null. eRPC emits rich diagnostic response headers (X-ERPC-Cache, X-ERPC-Upstream, X-ERPC-Duration, X-ERPC-Attempts, X-ERPC-Upstream-Attempts, X-ERPC-Upstream-Retries, X-ERPC-Upstream-Hedges, X-ERPC-Network-Attempts, X-ERPC-Network-Retries, X-ERPC-Network-Hedges, X-ERPC-Consensus-Slots, X-ERPC-Consensus-Disputes, X-ERPC-Consensus-Low-Participants, X-ERPC-Cache-Attempts, X-ERPC-Cache-Retries, X-ERPC-Cache-Hedges) — browser clients cannot read any of these without an explicit exposedHeaders list. Set e.g. exposedHeaders: ["X-ERPC-Cache", "X-ERPC-Upstream", "X-ERPC-Duration", "X-ERPC-Attempts"]. There is no wildcard shorthand accepted by current browsers.
cors.allowCredentials*boolfalseOnly when explicitly true is Access-Control-Allow-Credentials: true emitted.
cors.maxAgeint (seconds)3600 if 0Emitted as Access-Control-Max-Age only when > 0.
admin.cors*CORSConfigIf admin section exists and cors is nil: {allowedOrigins: ["*"], allowCredentials: false} then SetDefaults (safe because admin requires secret-token auth).Applied to /admin before any admin handling.

projects[].auth.* — AuthConfig

Dotted pathTypeDefaultBehavior
auth.strategies[]*AuthStrategyConfig— (required, ≥1)Evaluated in declaration order; first strategy that applies to the method, supports the extracted payload type, and authenticates successfully wins.
auth.strategies[].type"secret" | "jwt" | "siwe" | "network" | "database"— (required; unknown type → startup ErrInvalidConfig)Selects the strategy object; the matching sub-config must be present or startup fails.
auth.strategies[].ignoreMethods[]string (wildcard)nilStrategy skipped for matching methods.
auth.strategies[].allowMethods[]string (wildcard)nilRe-applies the strategy even when ignored (allow > ignore).
auth.strategies[].rateLimitBudgetstring"" (no auth-level limiting)Applied AFTER successful authentication. Per-user override User.RateLimitBudget (from secret.rateLimitBudget, siwe.rateLimitBudget, network.rateLimitBudget, JWT claim named by jwt.rateLimitBudgetClaimName (default "rlm"), or database record) takes precedence. Over-limit → ErrAuthRateLimitRuleExceeded (429). TryAcquirePermit is called with origin = "auth".
auth.strategies[].database.connector.idstringMust be unique across database strategies in one project; referenced by admin API-key RPCs (erpc_addApiKey etc.).

server.aliasing.* — AliasingConfig

Dotted pathTypeDefaultBehavior
server.aliasing.rules[]*AliasingRuleConfignilFirst rule whose matchDomain wildcard-matches the request Host (port stripped) wins; later rules ignored. Match errors are logged and the rule skipped.
server.aliasing.rules[].matchDomainstring (wildcard)e.g. "*", "eth.mycompany.com".
server.aliasing.rules[].serveProjectstring""Pre-selects projectId; path may still override with an explicit 3-segment path.
server.aliasing.rules[].serveArchitecturestring""Pre-selects architecture ("evm" or "svm").
server.aliasing.rules[].serveChainstring""Pre-selects the chain identifier — the numeric chainId for EVM, the cluster (mainnet-beta, or <chain>:<cluster> for a fork) for SVM. Combination project+chain WITHOUT architecture is rejected: "it is not possible to alias for project and chain WITHOUT architecture".

rateLimiters.budgets[].* — global rate-limit budget config

Dotted pathTypeDefaultBehavior
rateLimiters.budgets[].idstringReferenced by projects[].rateLimitBudget, auth.strategies[].rateLimitBudget, network/upstream budgets.
rateLimiters.budgets[].rules[].methodstring (wildcard)"*" if emptyA rule applies when the method matches (wildcard).
rateLimiters.budgets[].rules[].maxCountuint32Fixed-window limit per period.
rateLimiters.budgets[].rules[].periodsecond | minute | hour | day | week | month | yearsecond if invalid; YAML accepts names, ints 0–6, or duration aliases ("1s", "24h", "7d", …)Fixed window unit.
rateLimiters.budgets[].rules[].perIP / perUser / perNetworkboolfalseAdds ip/user/network descriptor entries so counters are scoped per client IP / authenticated user id / network id.
rateLimiters.budgets[].rules[].waitTimeDuration0Present in config; not consumed in the project-level path.

Legacy project fields (YAML-only)

These fields are read at load time, converted or warned, then erased. They are never visible after LoadConfig.

FieldTypeBehavior
routingStrategystringSemantic — triggers eval synthesis. "round-robin" → synthesizes rotateBy(ctx.tickCount) per network. Any other non-empty value → synthesizes sortByScore + stickyPrimary. Deprecation warning emitted.
scoreSwitchHysteresisfloat64Semantic — triggers eval synthesis even without routingStrategy. Non-zero → synthesizes sortByScore + stickyPrimary eval with stickyPrimary({ hysteresis: <value> }). Zero but another semantic field present → defaults to 0.10.
scoreMinSwitchIntervalDurationSemantic — triggers eval synthesis. Non-zero → synthesizes with stickyPrimary({ ..., minSwitchInterval: '<duration>' }). Zero but another semantic field present → defaults to '30s'.
scoreGranularitystringInert — warning only, no behavior change. Previously controlled per-upstream vs per-method scoring. Replaced by selectionPolicy.evalPerMethod.
scorePenaltyDecayRatefloat64Inert — warning only, no behavior change. Previously modulated penalty recovery. No equivalent exists.
scoreMetricsModestringInert — warning only, no behavior change. Previously configured metrics aggregation. Replaced by erpc_selection_* metrics with fixed cardinality.
scoreRefreshIntervalDurationInert — warning only, no behavior change. Previously set score-recalculation polling interval. Replaced by selectionPolicy.evalInterval.

Legacy translation mechanics. ProjectConfig.UnmarshalYAML captures all seven legacy fields. LoadConfig calls legacy.TranslateFromConfig before SetDefaults and Validate. Semantic fields present → per-network selectionPolicy.evalFunc synthesized UNLESS the network already has an explicit selectionPolicy.eval (modern fields always win). Inert fields only → warnings emitted, canonical default policy installs. After translation, LegacyProject is nil on every ProjectConfig — any code path that accesses it after LoadConfig always sees nil. Migration: remove all seven legacy fields and write an explicit selectionPolicy.eval using the chainable stdlib.

URL path → (project, architecture, chain) resolution

parseUrlPath cleans and splits the path; preselected values come from aliasing. Decision matrix (P=project, A=architecture, C=chain preselected):

  • None preselected: 1 seg → P; 2 segs → P + (network-alias resolve of seg2, else A); 3 segs → P/A/C; 0 segs → valid for healthcheck only; >3 → error must only provide /<project>/<architecture>/<chainId>.
  • P preselected: 1 seg → alias resolve else A; 2 segs → A/C; 3 segs → full explicit override.
  • P+A preselected: 1 seg → alias resolve (must yield both) else C; 3 segs → full override.
  • P+A+C preselected: only / (or healthcheck) allowed; >1 seg → error.
  • A+C preselected: 1 seg → P; 3 segs → full override.
  • A only preselected: 1 seg → P; 2 segs → P/C; 3 segs → full override; 0 segs allowed for global healthcheck.
  • P+C without A → error ("it is not possible to alias for project and chain WITHOUT architecture").

Post-checks: project required unless healthcheck; if A or C present, A must be "evm"; any non-POST/non-OPTIONS method becomes a healthcheck. /admin only with exactly one segment + POST/OPTIONS.

Network alias resolution. Aliases are registered eagerly for static networks and lazily when a network is first prepared. ResolveAlias(alias) returns (architecture, chainId) or ("",""). Duplicate alias with a different target keeps the first and logs a warning. Network ids must look like evm:<int>; alias misses in path-position fall through to being treated as an architecture, producing architecture is not valid for bogus aliases. If both path and body lack a network, the body's "networkId" field (e.g. "evm:42161") is consulted; otherwise: architecture and chain must be provided in URL ... or in request body ... or configured via domain aliasing.

Observability

All metrics use the erpc_ namespace.

MetricLabelsNotes
erpc_cors_requests_totalproject, originEvery request that carries an Origin header, before allow/deny. NOTE: the project label value is actually r.URL.Path, not the project id — cardinality grows with distinct request paths.
erpc_cors_preflight_requests_totalproject, originAllowed-origin OPTIONS preflights only. Same path-as-project label caveat.
erpc_cors_disallowed_origin_totalproject, originOrigin present but no pattern matched. Same path-as-project label caveat.
erpc_network_request_received_totalproject, network, category, finality, user, agent_nameFired in Project.Forward after project rate limiting — project-rate-limited requests are not counted here.
erpc_network_successful_request_totalproject, network, vendor, upstream, category, attempt, finality, emptyish, user, agent_nameVendor/upstream = "<cache>" for cache hits, "n/a" when upstream missing.
erpc_network_failed_request_totalproject, network, category, attempt, error, severity, finality, user, agent_nameerror = common.ErrorFingerprint(err).
erpc_network_request_duration_secondsproject, network, vendor, upstream, category, finality, userHistogram. vendor/upstream = "<error>" on failure.
erpc_rate_limits_totalproject, network, vendor, upstream, category, finality, user, agent_name, budget, scope, auth, originUnified deny counter. Project layer: origin="project", auth="". Auth layer: origin="auth", auth="<type>:<index>".
erpc_rate_limiter_budget_max_countbudget, method, scopeGauge.
erpc_rate_limiter_failopen_totalproject, network, user, agent_name, budget, category, reasonReasons: admission_full / limit_timeout. Monitor this to detect hard-enforcement gaps.
erpc_auth_failed_totalproject, network, strategy, reason, agent_nameIncremented inside auth strategies.
erpc_shadow_response_identical_totalShadow traffic comparison outcomes.
erpc_shadow_response_mismatch_totalMismatches log at error level with both hashes.
erpc_shadow_response_error_totalShadow request error outcomes.
erpc_network_evm_block_range_requested_totalBlock-range heatmap; emitted when a block number is extractable from the response.

Trace spans: Project.Forward (detail span), Project.PreForwardHook, RateLimiter.TryAcquirePermit (detail) and RateLimiter.DoLimit (client span with budget, method, scope, result attrs), Project.executeShadowRequest, and a request-level span via common.StartRequestSpan.

Notable logs: registered project <id> info at registration; successfully forwarded request for network (info); CORS request from disallowed origin, continuing without CORS headers (debug); skipping duplicate alias registration with different target (warn); no projects found in config; will add a default 'main' project (warn); no providers or upstreams found in project; will use default 'public' endpoints repository (warn).

Edge cases & gotchas

  1. Requests without an Origin header bypass CORS entirely — by design; eRPC treats CORS as a soft deterrent, not a security boundary.
  2. Disallowed origins are NOT rejected: non-OPTIONS requests proceed and get a normal JSON-RPC response, just without Access-Control-* headers; only the browser enforces blocking. OPTIONS gets a bare 204.
  3. . matches any single character in glob patterns (go-wildcard v2): allowedOrigins: ["https://erpc.cloud"] also matches https://erpcXcloud. Same applies to aliasing matchDomain and all wildcard fields. There is no escape syntax.
  4. forwardHeaders stores values under the configured pattern, not the matched header name — a wildcard pattern forwards upstream a literal header named like the pattern (e.g. X-Custom-*). Only exact-name patterns round-trip faithfully.
  5. Project rate-limited requests never increment erpc_network_request_received_total because AcquireRateLimitPermit runs before the counter; use errc_rate_limits_total{origin="project"} for those.
  6. scoreMetricsWindowSize stale source-code comments: two comments in the codebase say "10 minutes" (common/config.go:530, erpc/projects_registry.go:131-132), but the actual package-level variable is var ScoreMetricsWindowSize = 1 * time.Minute (source (opens in a new tab)). If you omit scoreMetricsWindowSize expecting a stable 10 m default, you silently get 1 m: the rolling window rotates every 6 seconds, upstream scoring reacts 10× faster than expected. Set scoreMetricsWindowSize: 10m explicitly to get 10 minutes.
  7. All rate limiting fails open: no store configured, Redis still connecting, Redis call timeout (limit_timeout), or admission semaphore full (admission_full). Deployments needing hard guarantees must monitor erpc_rate_limiter_failopen_total.
  8. allowMethods is not an allowlist by itselfshouldHandleMethod starts true; without ignoreMethods everything is served regardless of allowMethods. To build an allowlist: ignoreMethods: ["*"] + allowMethods: [...].
  9. Unsupported-method responses normally carry id: null due to an inverted error check before reading the request id in the error path.
  10. GET /myproject/evm/123 (or any non-POST/non-OPTIONS method) is a healthcheck, not an RPC error.
  11. OPTIONS to a project without cors configured is not short-circuited — the early-return lives inside the project.Config.CORS != nil branch, so the OPTIONS request falls into JSON-RPC body parsing and errors.
  12. CORS for a project can only run after project resolution succeeds: unknown project → 404 with no CORS headers, which browsers may surface as a CORS failure rather than 404.
  13. Lazy-loaded networks inherit networkDefaults and are appended into project.Config.Networks at runtime (visible via admin erpc_project).
  14. First aliasing rule wins; rule-match errors are logged and skipped. An explicit full 3-segment path can always override aliased preselection.
  15. Network aliases are per-project; a duplicate alias targeting a different chain is ignored with a warning, keeping the first registration. Aliases must be unique within a project's static config.
  16. Zero-project configs still validate because SetDefaults injects the main project before Validate runs.
  17. Project rate-limit test is fixed-window, not sliding — rate-limit rules apply to a fixed time window; requests over the limit in the current window are denied regardless of past windows.
  18. projects[].rateLimitBudget referencing an unknown budget fails startup — but auth-strategy budgets and per-user (database/JWT-claim) budgets are resolved at request time and surface as runtime ErrRateLimitBudgetNotFound.
  19. CORS metrics label project actually contains the URL path, so cardinality grows with distinct request paths and you cannot group by real project id from these counters alone.
  20. Secret auth via Authorization: Basic uses only the password half; the username is ignored. The deprecated ?token= query param still works and takes top precedence.
  21. Auth rate-limit denial still returns the authenticated user alongside the 429 error, so user labels remain populated on rate-limit metrics.
  22. exposedHeaders: nil silently blocks browser access to all X-ERPC-* headers. strings.Join(nil, ", ") returns "", so the server sends Access-Control-Expose-Headers: "". Enumerate desired headers explicitly — no glob wildcard shorthand is accepted by current browsers.
  23. Legacy scoring fields are consumed and erased before SetDefaults — after TranslateFromConfig runs, LegacyProject is nil on every ProjectConfig. Admin RPCs and health endpoints always see nil for this field.
  24. Inert legacy fields (scoreGranularity, scorePenaltyDecayRate, scoreMetricsMode, scoreRefreshInterval) do NOT synthesize an eval. A config with only these four fields gets deprecation warnings but retains the full canonical default selection policy (removeCordoned + keepHealthy + preferTag + sortByScore + stickyPrimary + probeExcluded).
  25. routingStrategy: "score-based" (or any non-"round-robin" non-empty string) synthesizes a sortByScore + stickyPrimary eval. A per-network selectionPolicy.eval (new field) always wins over the synthesized legacy eval.
  26. scoreSwitchHysteresis and scoreMinSwitchInterval are semantic fields that trigger eval synthesis even without routingStrategy. If either is non-zero, a sortByScore + stickyPrimary eval is synthesized across all networks.
  27. allowClientDirectives does not filter X-ERPC-Force-Trace — force-trace bypasses OTel sampling at span creation in StartHTTPServerSpan, which runs before project resolution. Filtering it requires deferring the sampling decision until after project resolution.
  28. allowClientDirectives does not affect directiveDefaults — config-set directive defaults (via networks[].directiveDefaults) always apply regardless of the client directive filter. The filter only gates directives arriving via HTTP headers or query parameters.

Source code entry points