jfastframework
Docs Español PyPI GitHub

Subsystem maturity

One version number cannot describe this repository honestly. The kernel is tested, typed and exercised by CI on every push; the Kafka client has never spoken to a broker. Calling both "0.7.0" told a reader nothing, which is why the package restarts at 0.1.0a1 and why maturity is tracked here, per subsystem, instead.

Levels

Level Means
beta Exercised by CI against the real thing. API may still change, behaviour is known.
alpha Tested in isolation, used by the author, not proven against production traffic.
experimental Shipped to get feedback. Expect the API to move.
unverified Written against documented APIs. Never executed against the real dependency.
broken A known defect, named below. Do not build on it yet.

Promotion gate: a subsystem does not move up a level because it feels more finished. It moves when a CI job exercises it against the real dependency — a container, a broker, a cluster. That is the same rule that let the Go scaffold ship and kept the Angular one out.

Kernel

Subsystem Level Notes
create_app, plugin registry, AppContext beta Full test suite, mypy --strict, entry-point discovery and cycle detection covered.
Settings and jfast.toml loading beta Typed, env-overridable. CORS, trusted hosts, body-size limit and request timeout are settings; all off unless configured.
RFC 7807 error model beta Handlers for domain, HTTP, validation and unhandled errors.
/health, /ready, /info beta Readiness runs every check concurrently under a timeout, and reports timeout apart from fail. /docs and /openapi.json close in production with /info. A plugin declared non-critical can no longer fail readiness, including when its check raises (0.1.0a11). Since 0.1.0a11 these routes, /metrics and the docs are matched after the application's routes, moved at startup and probed so an app route never takes one of their paths (tests/test_route_order.py).
Deadlines, breakers and boot validation alpha Every external call has a deadline and a breaker by default -- PostgreSQL connect and pool ping, Redis per command, JWKS, S3 per disk -- and boot refuses misconfiguration in database, cache, storage, mail, auth and queue, one test per rule (tests/test_boot_validation.py, tests/test_resilience_defaults.py). The failure drills pause a real PostgreSQL and Redis and hang an identity provider (tests/test_failure_drills.py); timings in docs/resilience.md, measured on one loaded laptop. CI runs the drills last in the main job from 0.1.0a11, and no CI run has executed them yet. No RabbitMQ drill; the settings of llm, tenancy, accounts, outbox, http, gateway and events are not validated at boot.
Performance budget alpha scripts/bench_overhead.py measures CPU per request in-process as a ratio to bare FastAPI; tests/test_performance_budget.py fails over 20 % growth and is proved to catch one BaseHTTPMiddleware. Runs only with JFAST_PERF_BUDGET=1. The checked-in baseline is darwin-arm64 only; the CI job measures the base branch on the same runner instead, and has not run yet. The telemetry plugin is not a budgeted scenario. See docs/scaling.md.
Test fixtures beta build_test_app, client_for, NullPlugin.

Plugins

Plugin Level Notes
observability beta JSON logs, request-id and tenant correlation. Zero dependencies. Plain ASGI since 0.1.0a10 (was BaseHTTPMiddleware, ~75 us per request).
metrics beta Route-template labels, so path parameters cannot explode cardinality -- claimed here since the start and true only from 0.1.0a10: the middleware read the route before routing ran and labelled by raw path. Now tested with path parameters, included routers, mounts and unmatched paths in tests/test_performance_guards.py. Plain ASGI since 0.1.0a10.
contracts (checker) beta Runs in CI against a generated service; a violation fails the build. Layers, forbidden calls and imports, required structure, and event-loop blocking. Since 0.1.0a11 events and tasks are part of the contract (publishes, orphan-subscription, undeclared-event, unused-dependency, and Job(task=...) across modules as undeclared-dependency), covered in tests/test_contracts_events.py.
database alpha Named connections, a read/write split with primary pinning, keyset and count-free pagination, per-tenant engines behind a bounded LRU, and a tenant filter that raises rather than failing open. The pinning and the pools are exercised against a real PostgreSQL, with the replica simulated as a second database that is deliberately behind — that reproduces lag, which is what breaks read-after-write, and not streaming replication or failover. No physical standby has ever been tested. Every pool number is per process and the image runs one worker per CPU: through 0.1.0a7 the defaults were 30 connections × 8 workers = 240 against a PostgreSQL that accepts 100, which nothing had multiplied. The default is 5 + 5 now and jfast check does the arithmetic against server_max_connections. The tenant LRU was probed under pressure: it holds its ceiling, never disposes an engine a request is holding, and raises rather than opening one past it. Through 0.1.0a8 the request session committed after the response was sent, so a failed commit answered 2xx; it commits before now, and the plugin refuses to start on a route that would not -- a service's own session dependency included, once marked @transactional; unmarked ones that commit after their yield are named in a warning. Constraint violations and stale versions are 409/412, and the races -- twenty concurrent inserts against a unique constraint, a counter under FOR UPDATE and under an advisory lock, a real serialisation failure retried to success -- run against PostgreSQL in tests/test_transactions.py. Since 0.1.0a11: connect_timeout 10 s, a bounded 2 s pool ping in place of SQLAlchemy's (which had no deadline), and a connect breaker; connect failures, dropped connections and a full pool answer 503. Drilled against a paused PostgreSQL: 503 inside the deadline, /ready naming database, recovery with no restart. pgbouncer = true turns off statement caches for transaction pooling, verified through PgBouncer 1.25 in tests/test_rls_pgbouncer.py.
db.rollups alpha MonthlyRollup: per-tenant monthly totals refreshed one bucket at a time, idempotent under redelivery, serialised on an advisory lock (without it ten concurrent refreshes hit a unique violation). SQLite and PostgreSQL in tests/test_rollups.py; measured on 3M rows in scripts/bench_aggregates.py.
cache alpha Redis facade and health check. Since 0.1.0a11 a 1 s deadline per command (blocking commands and pub/sub exempt) and a breaker; get_or_set fails open in one round trip. Drilled against a paused real Redis: first request 2 s, then 1 s, then immediate, all 200, /ready degraded, recovery with no restart. The drill's CI step has not run yet.
auth alpha Verification, JWKS rotation, refresh reuse detection and the refused-attack defaults are tested. Reuse detection was tested under a single-session assumption until 0.1.0a4 — the test asserted is_family_revoked(subject), which is the bug written as an expectation — so a logout revoked every session of that person and poisoned their next login. Families are per session now, and two-session cases are covered. Until 0.1.0a8 all of that was per process: without the cache plugin the store is in memory, the generated image runs one worker per CPU, and a logout revoked on one worker while the token kept working on the others — with refreshes sent to any other worker refused as revoked, a revocation that never happened. Production refuses to start in that configuration now, jfast check reports it at any environment, and both failures are reproduced in tests/test_session_store.py before the guard is asserted. Verification itself is exercised against a battery: a refresh token used as a bearer, a wrong signing key, alg=none, an expired token and a foreign audience are each refused. Still no PKCE, no mTLS, and no real identity provider in CI. Since 0.1.0a11 the JWKS fetch has a whole-attempt deadline, one retry, a breaker and shared failure for queued callers, drilled against a mock issuer that hangs (still no real IdP); auth reports degraded, not unavailable, while serving cached keys. Revocation fails open on a Redis outage by default (revocation_fail_open), so a revoked token works until it expires while Redis is down. Revocation and a refresh race are honoured across two replicas against a real Redis in tests/test_multi_replica.py. With the IdP down and no key ever cached, requests get 401, not 503.
accounts alpha Users, argon2id password login, lockout after repeated failures, roles and permissions carried as token scopes, per-tenant administration, and on_refresh so a removed permission or a deactivated account ends at the next refresh. Tested through HTTP on both SQLite and PostgreSQL in tests/test_accounts.py. Since 0.1.0a11: email verification (off/optional/required), password reset by email that ends every session, TOTP MFA with recovery codes and per-role enforcement, and sign-in rate limits with cache, tested over HTTP on SQLite and PostgreSQL in tests/test_accounts_flows.py and the limits against Redis. Not tested: the generated account pages in a browser, and delivery through a real SMTP server; the social-login callback in the generated frontend does not handle the MFA challenge.
tenancy alpha Resolution order is correct and tested. On its own the repository filter is a convention: a raw session.execute bypasses it. With [plugin.database] rls = true and enable_tenant_rls in a migration it is enforced by PostgreSQL row-level security: every transaction sets its tenant with a transaction-local set_config, and a query with no tenant sees no rows. Verified against PostgreSQL as a non-superuser role in tests/test_rls.py, including that the tenant does not survive into the next transaction on a pooled connection. Production refuses RLS under a superuser or BYPASSRLS role, where every policy is ignored. Policies on more than the tenant -- transaction_setting plus enable_rls_policy -- are verified the same way, with a tenant, its companies and a user who sees some of them. Behind PgBouncer 1.25 in transaction mode, verified locally in tests/test_rls_pgbouncer.py: the tenant does not survive into the next client's transaction on the same backend, and fifty interleaved transactions from two tenants never see each other; the CI job added in 0.1.0a11 has not run yet, and read replicas behind PgBouncer are not covered. The user source (each signed-in account is its own tenant) and current_tenant are covered in tests/test_tenancy.py. Since 0.1.0a11 jfast check has a tenancy check for settings that contradict each other, jfast check --multitenant-ready lists what a switch to several customers would break, and jfast tenancy enable writes the switch as one migration -- verified end to end on a generated service as a non-BYPASSRLS role (tests/test_tenancy_enable_pg.py). Not yet run on a real project of size; the readiness rules are heuristics tested one fixture per rule.
storage (local disk) alpha Key validation, symlink check, atomic writes, signed URLs, and an upload pipeline whose validate step sniffs the content type from the bytes rather than the filename — XML, JSON, CSV and text by parsing, with DOCTYPE/ENTITY refused — all tested. put_stream and guard_stream write uploads that do not fit in memory, validated on the first 64 KiB and capped as they arrive. Disk-independent key resolution and copy-on-read are off by default; the shipped ledger is in-memory and documented as dev-only. A local disk failure is critical for /ready. The "no signing key" warning appears only in production since 0.1.0a11.
storage (S3 / MinIO) alpha Verified against MinIO (RELEASE.2025-09-07T16-13-09Z) in tests/test_storage_minio.py: put, get, stat, exists, delete, listing past one page (it stopped at 1,000 until 0.1.0a11), signed URLs fetched and then expired, presigned upload, a three-part multipart put_stream and its abort on error and on cancellation, health. Timeouts, retries and the per-disk breaker are tested against doubles only; an object-store failure reports degraded. Never run against AWS S3. beta once the storage-minio CI job passes -- which runs a community fork (pgsty/minio) because MinIO stopped publishing images, and has not run yet.
encryption alpha AES-256-GCM with authenticated context and key rotation, on a column or by hand. Round trip, tampering, a value moved to another context, rotation and the column type are tested; key storage is the service's, through the environment or load_secrets.
web (Jinja + HTMX) alpha Rendered and smoke-tested; no browser-level test.
gateway alpha Prefix routing, header stripping, problem+json errors. One upstream per prefix: no pool and no load balancing. Rate limiting is the ratelimit plugin below; its README promised one for a year before it existed. Forwards W3C traceparent from its own client span when telemetry is exporting, and relays the caller's untouched when it is not.
ratelimit alpha Token bucket in a Lua script, so the read/decide/write is indivisible; the concurrency assertion was checked against a naive Python read-then-write first, where all 20 requests passed a limit of 5. Runs against a real Redis. Fails open by design, logging and reporting /ready degraded rather than turning a cache outage into a total one. Enforced as a FastAPI dependency, so a request matching no route is not limited — that is the edge's job. Not beta until CI has run it.
queue (worker, every backend) alpha A job takes the queuing request's tenant and request id, and the worker restores both around the handler; through 0.1.0a8 neither was ever filled, so every job ran with no tenant. enqueue still commits in its own transaction, apart from the request's rows. Covered by tests/test_transactions.py. Since 0.1.0a11: @task declared in the owning module with TaskSession and idempotent_on, local domain events (@subscribe) queued in the publishing transaction, and jfast worker, which drains for --grace seconds on SIGTERM and releases the rest without spending an attempt -- a real worker subprocess signalled mid-job in tests/test_worker_cli.py. jfast jobs dead / retry work on PostgreSQL and Redis. The worker has no health endpoint and does not reload under jfast dev; TaskSession always uses the primary database.
queue (PostgreSQL) alpha FOR UPDATE SKIP LOCKED, correct reclaim of jobs from a dead worker. Nothing extends the claim while a handler runs, so the handler has to finish inside visibility_timeout — through 0.1.0a7 the worker's job_timeout defaulted to the same 300 seconds from a different file, which raced at the boundary and ran the job twice past it. The worker derives its ceiling from the backend now and refuses a longer one. Delivery is at-least-once by design: ack follows the handler, so a crash mid-job redelivers, and handlers must be idempotent. Not run against a real PostgreSQL in CI.
queue (Redis) alpha Visibility timeout implemented: workers heartbeat into a registry on server time and any worker reclaims a dead peer's in-flight jobs. The heartbeat and reclaim are tested against an in-memory double of the Redis commands. Since 0.1.0a11 enqueue, dequeue, ack, dead letters and retry run against a real Redis 7 in tests/test_dead_letters.py and tests/test_multi_replica.py. A worker long-polling a paused Redis waits on the socket: BLMOVE has no command deadline.
queue (RabbitMQ) alpha Delays and retry backoff are held by the broker in a cascade of fixed-TTL queues, so a long delay cannot block a short one. Verified against rabbitmq:3.13 in tests/test_rabbitmq_queue.py: delay, due order, head-of-line, retry backoff, dead-lettering, delays longer than the cascade, and a worker retrying a failing handler. CI requires it not to skip, but no CI run has executed it yet. Not tested in a cluster, across a broker restart, or with quorum queues; a broker crash during a hop between levels can lose that message.
queue scheduler alpha Every replica runs it; ticks are claimed in PostgreSQL (jfast_schedule_ticks) or Redis, and the job id is deterministic. Verified against real PostgreSQL 16 (20 concurrent claims land once, claim and job commit together, two running apps enqueue each tick once, pruning) and real Redis 7 in tests/test_scheduler.py; cron and DST covered by 77 unit tests. A process killed between claiming and enqueueing loses that tick when the store and the queue are different systems. No CI run has executed the real-server tests yet. Since 0.1.0a11 two replicas enqueue each tick once on both the SQL and the Redis tick store (tests/test_multi_replica.py).
http (service client) alpha Timeouts, retries, retry budget, circuit breaker, bulkhead and header propagation are tested against httpx.MockTransport with a fake clock. Never sent over a real socket; no TLS, no load test; streaming not supported. Breakers are per process. Sends W3C traceparent/tracestate inside a client span when telemetry is exporting; tested with two apps over an ASGI transport, not a socket.
outbox alpha Messages written through the request's session commit with its rows or not at all; on the PostgreSQL queue in the same database the job goes straight into jfast_jobs. The relay claims rows with FOR UPDATE SKIP LOCKED, backs off, and sets a message aside as dead after max_attempts; claim_once deduplicates on the consumer's side. Two relays racing over 60 rows send each exactly once against PostgreSQL in tests/test_outbox.py. Since 0.1.0a11 publish queues one job per local @subscriber in the same transaction and refuses an event nobody will receive (UndeliverableEvent); /ready is degraded from the first failed attempt. Run against a real Redis queue: two relays and two workers, 400 messages relayed and consumed once each (tests/test_multi_replica.py). Local events over the Redis or RabbitMQ queue are tested with a recording queue only, and the relay has not been run against a real RabbitMQ or Kafka.
idempotency alpha Idempotency-Key recorded in the request's transaction: a retry replays the stored response, a different body with the same key is 422, a concurrent duplicate waits on the first insert and gets 409 or the replay. Four simultaneous requests with one key write one row against PostgreSQL in tests/test_idempotency.py. The response is recorded after it is sent, so a process that dies in between leaves the key in progress until it expires.
events (Kafka) unverified Never run against a broker. No outbox, so a committed row can lose its event. Since 0.1.0a11 @on handlers restore the event's tenant, request id and trace; still never run against a broker. Local events between modules no longer need this plugin.
mongo alpha Client and handle only. No document contract, no migrations, no repository parity with SQL.
qdrant alpha Client, health check, container.
rag alpha Tenant-scoped identity, tenant required on every call, HNSW, hybrid search with reciprocal rank fusion, structure-aware chunking and re-ingest that embeds only changed chunks; schema_sql() for a migration instead of DDL at startup. The pgvector store is verified against PostgreSQL 16 + pgvector 0.8 in tests/test_rag_pgvector.py, including an in-place upgrade of a 0.1.0a9 table and row-level security with a non-superuser role. The Qdrant store is not run against a server: it follows the same protocol and is type-checked, nothing more. No reranking, no OCR, no hybrid search on Qdrant. Measured in 0.1.0a11 (scripts/bench_rag.py, synthetic embeddings): 300k chunks across 1,000 tenants, vector p50 2.54 ms / p99 6.92 ms, hybrid p50 2.96 ms -- but at that size the planner uses the tenant btree and an exact sort, so HNSW serves no tenant-scoped query; with four large tenants it does, at recall@10 0.918 with the default ef_search. Document writes use the tenant index (tests/test_rag_scale.py asks the planner). Not run at 1M chunks.
llm alpha OpenAI-compatible chat, strict JSON output, images and embeddings. The budget reserves the worst case atomically before a call and settles to the real cost; a failed call releases it. Tested with httpx.MockTransport in tests/test_llm.py; the Redis ledger was run against Redis 7 across two replicas in tests/test_multi_replica.py: under a $1.00 cap, exactly 10 of 60 concurrent $0.10 calls went through. Never called a real provider from CI. No streaming.
notifications (FCM) unverified Payload construction is tested; a delivery has never been made from CI.
channels alpha Declared pub/sub. The memory backend is covered by tests; the redis and kafka backends are not run against a real server in CI.
mail alpha Templates, queueing and the console backend are tested. Through 0.1.0a7 a service could ship to production on the default console backend, which prints every message to stdout while send reports success — no bounce, no error, and no mail; production refuses both silent backends now. No message has been sent through a real SMTP server from CI, which is the reason this is not beta: the backend that actually delivers is the one nothing exercises. Since 0.1.0a11 a 5xx reply, an oversized message or malformed headers go to the dead letters on the first attempt while timeouts and 4xx keep their retries -- tested against smtplib exceptions, not a server -- and the settings are validated at boot.
websocket alpha Handshake, registry, backpressure, heartbeat and token expiry are covered in isolation. Cross-worker delivery is asserted against a real redis:7-alpine — two app instances, a socket on each, exactly-once — and the author mutation-tested that assertion, finding it passed with a double-delivery bug injected before fixing it. CI now starts Redis and fails if the test skips, but no CI run has executed it yet, so this stays alpha until one does. No browser client, no proxy, no load test, and two workers in one process rather than two processes.
sentry alpha Off by default.
telemetry alpha OpenTelemetry traces over OTLP: requests by route template, SQL without parameters, http and gateway calls, llm and rag with counts and cost, jobs and events inside the request's trace. Nothing installed and nothing measurable without an endpoint (41 us per request either way, measured in-process). Tested with the in-memory exporter in tests/test_telemetry.py, SQL spans against PostgreSQL, two apps in one trace over an ASGI transport, and a broken tracer, backend and exporter never breaking a request. The generated collector + Jaeger pair was checked by hand; the telemetry CI job that exports to a real Jaeger was added in 0.1.0a11 and has not run yet -- beta once it passes. Not tested: a hosted backend, TLS to the collector. FastAPI 0.142's own telemetry is turned off.

Generator and deployment

Area Level Notes
jfast new module / new service (Python) beta Every layout (modular by default), Spanish or English table names, the HTMX overlay, Alembic and pytest wiring are rendered and run in CI. Since 0.1.0a11 every layout in example, --fields and --bare form passes ruff, ruff format, mypy --strict and pytest in scripts/smoke_generated_quality.sh, run locally on Python 3.12 only; the generated-quality CI job has not run yet. The HTMX overlay does not understand declared fields and is refused with them.
jfast start beta Through 0.1.0a6 it generated a module it never mounted, so the service answered 404 on its own endpoints while every test passed, and the compose file it wrote could not build because no generator wrote the Dockerfile that build: needs. Nothing caught either: the tests covered the pieces and nothing ran the sequence. Promoted because CI now does — scripts/smoke_compose.sh runs docker compose up --build on what this command generates and asks the running container what it serves, and the compose job passed on 0.1.0a7. tests/test_quickstart.py holds the same assertions statically. Since 0.1.0a11 the single-tenant default and --multitenant both pass jfast check --ci, and the multitenant project passes --multitenant-ready with zero findings (same script).
jfast init alpha The plugin menu is built from PLUGIN_CATALOG and the multitenant question sets every tenant setting at once; both unit-tested. The interactive session itself is not driven end to end.
jfast add / jfast remove (plugins) alpha Tested in tests/test_installer.py, including required plugins, extras and comments kept. add skips pip when the pin differs from the running version or the install is editable.
jfast worker and generated worker deployments alpha The worker is a real subprocess signalled mid-job in tests/test_worker_cli.py; the generated compose worker passed docker compose config; the Kubernetes Deployment is parsed and string-checked only. jfast dev starting it is tested with a stubbed spawn.
jfast tenancy enable / jfast check --multitenant-ready alpha End to end on a generated service against PostgreSQL as a non-BYPASSRLS role; --not-null checked for its SQL, not applied. Readiness rules are heuristics, one fixture per rule, with documented blind spots.
jfast bench alpha Step load test from the OpenAPI, tested against a uvicorn service of known capacity in tests/test_cli_bench.py. Its generator tops out near 3,000-3,600 req/s; it does not name the saturated dependency beyond /ready.
Upgrade smoke (scripts/smoke_upgrade.sh) alpha 0.1.0a10 → this checkout, run locally. Fails today by design until upgrades.py names hexagonal-eager-create-payload; the upgrade-smoke CI job has not run yet. Ten detectors have known bugs pinned by strict xfails in tests/test_upgrade_detectors.py.
Vue and React scaffolds alpha npm install plus vite build run in CI, which is what caught the router marker bug. Both looks -- nexora (the default) and classic -- are built in both frameworks by smoke_frontend.sh, and the look is recorded in .jfast-template and followed by jfast new view. The Nexora screens and accent picker were checked by hand in Chrome only: not Safari or Firefox, and the no-WebGL fallback has not been seen. No runtime test. Since 0.1.0a11 they speak accounts (sign-in with MFA, register, verify, reset, Security page); all four look-framework pairs built with npm run build, none of the account pages clicked through in a browser.
Go service scaffold alpha "Contract yes, framework no": standard library only, plain net/http middleware. CI generates it (plain and --grpc), runs gofmt, go vet, go test and go build, starts the binary and curls it. Since 0.1.0a11 it passes W3C trace context through (validated, logged as trace_id, forwarded unchanged; no spans), verifies the workspace's JWTs with the Python auth plugin's rules and variable names (secret and public_key; jwks refuses to start) and resolves the tenant as tenancy does. Proven across languages in tests/test_go_service.py: tokens minted by the Python TokenIssuer (HS256, RS256) are accepted and resolve the same tenant a JFast app resolves, refresh and expired tokens get 401 on both sides, and a traceparent reaches the outgoing call unchanged -- run locally in golang:1.23; the CI step has not run yet. Revoked tokens are not checked (15 minutes of life after a logout, by default). Not verified: behind the gateway with auth on, ES tokens from Python, a real identity provider, a Go consumer of the queue (the format is documented, no client ships).
gRPC experimental The .proto contract is generated and the port reserved. No stubs, no server wiring.
Workspaces and port allocation beta Resources are named instances and services bind to them under a variable; the whole flow is exercised in CI. A 0.1 file still loads and migrate-resources converts it.
deploy compose / dockerfile beta Derived from the plugin graph. One container per resource with the connection strings beside them, and the generated image is built and run against a real PostgreSQL in CI.
workspace k8s unverified Manifests are structurally asserted in tests. They have never been applied to a cluster, not even kind. Since 0.1.0a11 they include a <svc>-worker Deployment when the queue is on, equally unapplied.
deploy function (Lambda / Cloud Run) unverified Writes scripts rather than running them; never applied against a real account.
load_secrets (AWS / GCP) unverified Never run against a real secret manager.

Not present at all

Named here so nobody has to grep to find out:

PLAN-NEXT.md is the ordered plan for closing all of it.