Skip to content

Architecture

How a request flows through Felix end-to-end. Source of truth: felix-run/felix.

Client → Ingress (Caddy / Traefik / nginx / CDN)
│
▼
felix-api (CPython 3.14, Granian, FastAPI) ─┐
felix-worker (Taskiq consumer) ─┤ no ingress
felix-scheduler (Taskiq cron enqueue) ─┤ no ingress
▼
Postgres+pgvector · Valkey · object store (fs | s3 | gcs | memory)

In words: requests reach an ingress (Caddy, Traefik, nginx or a CDN), which routes to felix-api only. felix-worker and felix-scheduler run alongside it with no ingress of their own. All three connect to the same Postgres with pgvector, Valkey, and object store (fs, s3, gcs or memory).

Process Role
API (apps/api) HTTP surfaces, OpenAPI//docs, agent invoke/stream
Worker (apps/worker) Audit flush, jobs, memory consolidation, retention, anomaly, continuous eval, fiber resume
Scheduler Enqueues labeled Taskiq cron tasks (required alongside the worker)

The harness library lives in packages/harness (felix). The CLI is apps/cli (felix migrate|doctor|eval|mint-jwt|…).

Felix is plain CPython in containers you run. There is no serverless or edge runtime in the stack, and no vendor-specific compute or storage binding — Postgres, Valkey and an object store sit behind Protocols. Deploy with Compose or Helm — see getting started and deploy.

create_app (apps/api) mounts:

Surface Paths
Chat POST /chat, /chat/stream, /chat/steer, /chat/fork, /chat/rewind, GET /chat/runs/{resume_token}
OpenAI-compatible POST /v1/chat/completions, GET /v1/models
A2A POST /a2a
MCP POST /mcp
Discovery GET /.well-known/agent-card.json, JWKS when configured
Management /audit, /approvals, /plans, /jobs, /manifests, /eval, /usage, /memory, /documents, /artifacts
Internal POST /internal/* (consumer secret when auth ≠ none)

Interactive OpenAPI UI: a running harness serves Scalar at /docs. Swagger and ReDoc are both off, so there is one reference surface under one CSP — and it is authenticated unless FELIX_DOCS_PUBLIC=true. The public copy is this site’s /reference/, built from the openapi.json attached to the release production runs.

The manifest write verbs (PUT, canary, rollback, clear-canary) are not registered at all under FELIX_MANIFEST_SOURCE=bundled, so they are absent from the OpenAPI document rather than present and refusing.

Order on every request, outermost first:

  1. Request id — every response carries x-request-id, including one rejected downstream.
  2. Security headers — on every response, a 413 and a 401 included.
  3. Body limit — reject oversized payloads before any other work.
  4. Rate limit — keyed by tenant; skips only the probes (/health, /live, /ready) and /.well-known/*.
  5. Auth — none / api_key / jwt (see auth); installs RequestContext (tenant, scopes, LimitState).

Rate limit is outside auth, and that is the point. Registered the other way round, a 401 returned before the limiter was consulted and credential guessing was entirely unthrottled. For the same reason /docs, /openapi.json and /metrics are not exempt: under api_key / jwt those were three more paths where a credential could be guessed at no cost.

Self-authenticating mounts (e.g. /internal) skip bearer parsing; the router enforces its own credential (FELIX_CONSUMER_SHARED_SECRET).

POST /chat or /v1/chat/completions
→ auth + rate limit
→ resolve_tenant_manifest (tenant Postgres → object store → bundled)
→ prepare_tenant_invoke
enforce_inbound_auth (spec.auth.inbound: anonymous / required scopes)
ensure_thread_pin (spec.governance.pin_compile → 409 if the pointer moved)
→ apply_inbound_screening on the caller's turn
→ build_agent(manifest, deps)
validate → system prompt + skills
bind MCP / peers / sandboxes / containers / queues / browser
governance wrappers (secret masking → … → approvals)
get_pattern(spec.pattern) → Agent
optional durable wrap (fibers)
→ agent.invoke / agent.stream_events
session strategy renders working set
model chat → tool dispatch → persist session events

Names are the actual callables: felix.runtime owns resolve_tenant_manifest and prepare_tenant_invoke, felix.manifests.builder owns build_agent, and invoke / stream_events are the two methods on the Agent protocol in felix.patterns.types.

The durability backend is fibers (Postgres-backed sleep/step/resume; each worker polls for due fibers every second and advances up to eight at once, with a once-a-minute cron sweep as the backstop). Set spec.execution.mode: durable to enqueue and return 202 with a resume_token.

felix-scheduler enqueues labeled cron tasks; felix-worker consumes them. Run both, or none of the following ever fires:

Task (apps/worker) Cron Does
flush_audit */1 * * * * Drain the audit buffer to Postgres (and the optional warehouse spill)
flush_usage */1 * * * * Drain buffered token usage
run_scheduled_jobs * * * * * Sweep due jobs rows
fiber_scheduler * * * * * Wake and resume due durable fibers
webhook_delivery * * * * * POST finished durable runs to their completion webhooks, with retry
skill_jobs * * * * * Run accepted skill improvements (each into a draft) and queued skill evaluations, at most five of each per sweep
continuous_eval */10 * * * * Continuous eval tick
skill_upstream_checks */10 * * * * Check up to 50 imported skills due under FELIX_SKILL_IMPORT_CHECK_HOURS against their origins; off at 0
skill_update_notifications * * * * * Send queued skill.update_available events to the endpoints FELIX_SKILL_UPDATE_WEBHOOKS binds to each tenant: up to 50 a tick, at most 10 of one tenant’s, one sweep at a time on its own skill_job_lease row
consolidate_memory */15 * * * * Memory consolidation
anomaly_scan */30 * * * * Error-rate scan and canary auto-rollback
retention_sweep 0 3 * * * Prune expired rows and objects
Path Concern
packages/harness/src/felix/ Manifests, patterns, tools, session, governance, auth, plugins
apps/api FastAPI composition + routes
apps/worker Taskiq worker and scheduler entrypoints
apps/cli Ops CLI
manifests/ Bundled agents (quick, deep, router, …)