jfastframework
Docs Español PyPI GitHub

Changelog

Format: Keep a Changelog. Versioning: SemVer with pre-release identifiers per PEP 440. While the API is pre-alpha, services pin exactly (jfastframework==0.1.0a5); a compatible-release pin (~=) starts making sense at 0.2.

Renumbering

The entries below were originally numbered 0.1.0 through 0.7.0. That numbering overstated the maturity of the code. Nothing had been published at the time; the workspace file format is about to change; the Redis queue backend does not implement the visibility timeout its own contract documents; the RabbitMQ and Kafka backends have never been run against a real broker.

The package therefore restarts at 0.1.0a1. The history is kept verbatim as a development log — it records what was built and when — but those numbers were never releases and were never installable. pip install jfastframework will not resolve a pre-release without --pre, so the packaging tool enforces the warning rather than a sentence in a README.

Subsystem-level maturity lives in STATUS.md, which is the file to read before depending on any single part of this.

[Unreleased]

[0.1.0a12] - 2026-09-30

What building a new SaaS from scratch on 0.1.0a11 ran into, fixed. None of it was visible to the test suite, because each needed the path a new user takes: an optional unique field, a production image, the generated frontend in a browser, an upload, a migration run on the host, a model call from a worker. One addition came with it: enums in --fields.

Fixed

What a help desk built from scratch on this release hit in production containers, fixed before it shipped:

Added

[0.1.0a11] - 2026-09-30

Built from two real services on 0.1.0a10, Cuadra and Dictamen, and from what they had to work around. The worst finding broke a rule this project had not written down: the contracts told a module to publish an event instead of calling another module, and in the default stack that event went nowhere -- outbox.publish answered 201 and the row retried until it died. So this release is mostly about making the recommended path the one that works: events that reach a subscriber without a broker, a worker that exists, accounts a SaaS can open with, a switch from one customer to several that is a command and not a hunt, a deadline on every call to something that can hang, and generated code that passes its own checks.

For every breaking item below, run jfast upgrade --check: it lists the ones that apply to your project, with file and line, and the remedy.

Breaking

Added -- events and background work

Added -- telemetry

Added -- accounts

Added -- one customer today, several tomorrow

Added -- deadlines, breakers and failure drills

Added -- generated code that passes its own gates

Added -- Go services in a Python workspace

"Contract yes, framework no": the Go scaffold gets what a Go service needs to sit next to Python ones -- still standard library only, all of it plain net/http middleware that wraps a Gin, Echo or Chi engine as it is.

Added -- CI

Changed

Fixed

Performance and scale, measured

Every number here is in docs/scaling.md, new, with the machine it was measured on (an Apple M5 laptop with other suites running) and the script that produces it.

Not done, and named

[0.1.0a10] - 2026-09-29

Three things a project outgrows in its first month of real use: retrieval that mixed tenants, modules that talked to each other through raw SQL because the contracts forbade every other way, and a tenant model with no place for "each account is its own".

Breaking

Fixed

Performance

Measured with ab on one uvicorn worker (table in docs/deploy.md#performance): a service with auth, tenancy, metrics and logs went from 2,411 to 8,581 requests a second on the same endpoint; FastAPI with JWT and tenant written by hand does 9,494. JFast with its default plugins went from 4,228 to 12,443.

Added

Changed

[0.1.0a9] - 2026-09-28

A 201 has to mean the row exists.

The request's session committed in dependency teardown, and FastAPI runs a yield dependency's teardown after the response has been sent. So a commit that failed had already been answered with success, and a client that read its own write straight away could get there before the commit did. Every generated module wired its session that way.

Breaking

Added

Added -- the rest of the transaction story

Changed

Fixed

Fixed -- framework tables and autogenerate

Changed -- the CLI

Added -- what a real service needed

Found by rebuilding an invoicing service (E-Cont) on the framework: each of these it had written itself, and each one it got wrong in the same place.

Added -- recurring tasks, delays on RabbitMQ, calling other services

Fixed -- RabbitMQ

Added -- frontends come in looks, and Nexora is the default

Changed -- modular by default

Documentation site

Not done, and named

Distributed tracing, and, in accounts, email verification, password reset and MFA. Both in PLAN-NEXT.md.

[0.1.0a8] - 2026-09-03

One process is not the shape this runs in.

Every number in this framework describes a single process -- a token store, a connection pool, an engine map. The Dockerfile it generates ends in uvicorn --workers $JFAST_WORKERS, one per CPU. Nothing had ever multiplied the two, so the arithmetic that decides whether a deployment works was never done.

Added

Changed

Fixed

Audited, and found correct

Written down because "we looked" is worth as much as "we fixed" to whoever reads this next, and because each of these was checked by running it:

Fixed -- the session, which was also per process

Fixed

Added earlier, written down here

These five shipped in 0.1.0a6 and never got an entry: the notes for them sat under [Unreleased] while two releases were cut below them. Recorded now under the release that noticed, rather than backdated into one that did not say it.

Five commands that move the CLI past the first ten minutes of a project. Each answers a question the framework could already have answered and did not.

[0.1.0a7] - 2026-09-03

The quickstart, made to survive being followed literally.

Four commands end in a panel telling somebody what to run next. On 0.1.0a6 three of those lines did not work on a project the framework had just written: docker compose up --build had no Dockerfile to build, the api container it did start read the host's DSN and crash-looped, and jfast start shipped a module it never mounted, so the endpoints its own generated tests covered were absent at runtime.

Every piece worked in isolation, which is why 1302 tests were green while the first ten minutes were not. The release that fixes them also adds the two checks that would have caught them: tests/test_quickstart.py, which asserts the sequence rather than the pieces, and scripts/smoke_compose.sh, which runs the generated compose files and asks the running containers what they serve.

Added

Fixed

[0.1.0a6] - 2026-09-03

Hardening, in the three places where "it works here" and "it works" are different claims: another operating system, inside a container, and behind a proxy. Nothing in this release is a feature; all of it is a case that was not covered.

Added

Fixed

Twenty-six files, 1,068 insertions, and eleven of the twenty-six are tests.

[0.1.0a5] - 2026-08-30

Twenty findings from a second external report, this one raised against 0.1.0a4. Nine were verified by hand before anything was touched, two turned out worse than reported, and two were rejected -- one of them rejected as a defect and answered as a naming problem instead.

The theme is narrower than last release's. 0.1.0a4 built seven diagnostic commands in a day and three of them lied: migration check approved three dangerous statements, contracts check passed on a contract that governed no file at all, and analyze reported no findings on that same project while jfast next said the check had failed. A diagnostic nobody believes gets ignored; a diagnostic people believe and that is wrong costs more than the defect it was built to catch, because it is the reason they stopped reading the migration by hand.

Two rules came out of that, and both ship as tests rather than as notes:

  1. No diagnostic is trusted without a case where it must fail and does.
  2. A test asserts the half that can break, by the route a user takes -- not the advertised string when what fails is the port mapping, not contracts init when people run jfast new service, not the middleware in isolation when it runs behind uvicorn.

1,287 tests pass and 7 skip: five want a real Redis, two are one command that is deliberately reachable two ways.

Breaking

A file that matched a layer yesterday and matches none today is governed by nothing, forbid_packages included. Where the reach was intended, widen the pattern:

diff -paths = ["modules/*/repository.py"] +paths = ["modules/**/repository.py"]

** crosses directories on purpose and says so, and **/ also matches zero segments, so modules/**/http.py still covers modules/http.py. jfast upgrade --check lists the files that changed hands under layer-globs-narrowed, computed against your tree rather than read off the patterns: a contract written in ** throughout is unaffected and gets no report.

diff -created = datetime.now() +from jfastframework.time import now +created = now()

datetime.now(UTC) is equally fine: the rule is about the missing argument, not about which module you reach for. A *args or **kwargs splat is never reported, because the syntax cannot say whether the zone is in there. To defer the whole rule, one line in contracts.toml:

toml [rules.async_safety] naive_datetime = false

That leaves the async-blocking half on, which is the half you already had.

toml [plugin.database] session_timezone = "" # leave the server's TimeZone alone

Sent in the startup packet and not as a SET after connect, deliberately: a pooled connection is checked out mid-life, so one DISCARD ALL, one RESET ALL or one pgbouncer server-reset undoes a statement that ran once and the session falls back to the server's zone with nothing in the logs.

For projects generated before this, the consequence is a new failure: contracts check reports layer-unmatched (exit 5) for a layer that matches no file while files it should have claimed go unclaimed. Point the layer at the folders your modules really use --

diff [layers.storage] -paths = ["modules/*/repository.py"] +paths = ["modules/*/infrastructure/*.py"]

-- and jfast inspect names each module's layout. jfast contracts init --layout X --force also fixes it and overwrites the whole file, every layer, rule and waiver the project added included, so it is the last resort here rather than the first.

diff -async def rotate_refresh(self, token_id: str, *, family: str, ttl: int) -> bool: +async def rotate_refresh( + self, token_id: str, *, family: str, ttl: int, grace: int = 0 +) -> RefreshOutcome:

RefreshOutcome is Literal["rotated", "raced", "replayed"] in jfastframework.auth.store, where MemoryTokenStore and RedisTokenStore are worked examples. A store that cannot honour a grace window returns "replayed" wherever it used to return False. Only a project that wrote its own store is affected; the shipped ones are read correctly by the framework.

diff class PageResponse(BaseModel): - total: int + total: int | None

has_more answers "is there a next page" without a total. jfast upgrade --check lists the call sites under pagination-total-optional.

diff -docker exec -it shop_postgres psql -U shop +docker compose exec postgres psql -U shop

Added

jfastframework.time answers the half that is a business question. now() is the only clock the framework reads and is always aware UTC; today(tz) and day_bounds(day, tz) decide which local day a UTC instant belongs to. day_bounds returns a half-open [start, end) in UTC -- never BETWEEN, whose closed upper bound either double-counts midnight or drops the last microsecond depending on the column's precision -- and resolves both edges with fold=0, which is what makes it right on the local day that has no midnight (America/Santiago starts DST at 00:00) and on the one that has two. in_zone() renders and is presentation only; parse() refuses a string with no offset unless the caller says which zone wrote it.

Three zones, named apart because they are three questions: storage is UTC and is not configurable; [app] timezone is the business zone, what "today" means for a report, an invoice period or a daily quota; [plugin.tenancy.timezones] overrides it per tenant, which is the case one deployment serving several countries exists to handle, with a tenant_zone dependency that falls back to the business zone and never to the server's. Every name is validated at boot, so a typo stops the service instead of shifting one tenant's reports by a day a week later.

"UTC" resolves to datetime.UTC and reads no file: zoneinfo reads /usr/share/zoneinfo for that key like any other, and a slim container with no tzdata raises for every name. Naming a real zone on such an image fails at boot with a message that says which package is missing.

jfast doctor reports db_timezone as a pair -- what an unpinned client computes in, and what this service computes in -- read over two connections because a startup parameter becomes the session's reset value, so pg_settings.reset_val reports UTC on a server set to anything. A server that is not UTC is not this service's failure and is still worth one line: every other client of that database, psql and a BI tool and a migration run by hand, is reporting a different day. See docs/timezones.md.

It still runs none of them, on purpose: pytest executes your code for an unbounded time and mypy in a tree whose dependencies are not installed manufactures findings, and either one inside this command turns a pre-commit hook into a build. So the answer is naming rather than adding. Every run ends with

not checked here: lint, formatting, types, tests ruff check . ruff format --check . mypy . pytest

pass or fail, and --json carries the same four under not_covered, each with what it catches and the command that catches it.

layers domain 4 files application 2 files infrastructure 3 files adapters 2 files shared 2 files

A layer at 0 is the finding, and it is the one this checker reported late: layer-unmatched fires only once files that layer should have claimed are also unclaimed, and a layer that drops from 40 files to 3 in a refactor raises nothing at all. Counted through layer_for rather than by raw globbing, so a layer whose every match is taken by a more specific pattern shows as the zero it is.

One fact, one owner, across four commands: analyze reports contract-governs-nothing by calling check_coverage rather than reimplementing it -- it used to print "no findings" on a project whose contract governed nothing while jfast next said the check failed -- contracts diff lists what an empty layer permits under ~ with the reason instead of selling an outage as ten opportunities to tighten the contract, and next collapses the pair into one step rather than five lines about one thing to do.

Fixed -- silent

jfast serve, jfast dev and the generated Dockerfile entrypoint all pass --no-proxy-headers now: one resolver in the process, and it is the one that reads jfast.toml. A peer that arrives already substituted is caught rather than believed -- a proxy appends the address it received the connection from, never its own, so a genuine transport peer does not appear in the chain it is relaying -- and such a request is treated as having no client at all, one bucket an attacker cannot rotate out of, with the scheme downgraded when the header could have written it. The tests for both halves run against a real uvicorn on a real socket, started the way it ships.

Every ordering the repository builds now spells NULLS LAST on the nullable columns, because the default is not one thing -- PostgreSQL puts NULLs last ascending and first descending, SQLite puts them first either way -- and the keyset comparison is written against that: IS NULL for a tie, and a step past a non-NULL value admits the NULL block that follows it. The cost is stated rather than hidden: a nullable ordering column demotes the index range scan to an index scan with a filter, measured at 840 buffers against 4 with NOT NULL columns, one page at depth 100k of 200k rows, against 1,049 for the same page by OFFSET. Still the cheapest of the three ways to page, no longer flat.

Fixed -- correctness

[0.1.0a4] - 2026-08-30

Twenty defects found by building a real application against 0.1.0a3, plus a security audit and a review of the CLI. Almost every one of them failed in silence: a consumer that subscribed to nothing, a refreshed token that authenticated and was authorized for nothing, a compose file that regenerated identically while ignoring a plugin, a contract that forbade the pattern its own documentation prescribes.

Breaking

sql ALTER TABLE invoices ALTER COLUMN created_at TYPE timestamptz USING created_at AT TIME ZONE 'UTC', ALTER COLUMN updated_at TYPE timestamptz USING updated_at AT TIME ZONE 'UTC';

Alembic's autogenerate writes the bare form with no USING. That does not fail -- it converts through the implicit cast, reading every stored value in the server's TimeZone, and silently shifts the whole table on any server not set to UTC.

Fixed — silent

Fixed — correctness

Added

Added

inspect is one screen: modules, how each is shaped, what it serves, whether it is wired into the app. inspect module <name> goes deeper. analyze reports eight structural problems worst-first, each with the fix on its own line -- import cycles, a module main.py never registers, two routers claiming one prefix, shared/ importing a module, an enabled plugin nothing provides. graph draws the module dependency graph as text, mermaid, dot or JSON.

Every check is decidable from the source. Nothing guesses: a checker that is right nine times in ten gets muted after the second false positive, and the true findings go with it. module-no-migration fires only once there are revisions, so a freshly generated project reports nothing.

Changed

bg-surface, bg-panel, bg-elevated, border-line, text-ink, text-ink-soft and text-ink-faint now come from @theme inline, so the utilities emit var(--ui-*) and one variable swap flips the interface. Not one component carries a dark: class any more, except the four status hues on BaseBadge and ToastHost where the colour is the meaning. color-scheme is set alongside, so scrollbars and date pickers follow too.

Fixed

forbid_packages = ["sqlalchemy"] now ships on layers.http (layers.adapters in hexagonal) in every contract template. The four layouts still generate and pass their own contract; the planted import now fails with layer-package: 'http' must not import 'sqlalchemy' and exit code 1.

Existing projects are unaffected -- contracts.toml is yours once generated. Add the line to adopt the rule.

Added

[0.1.0a3] - 2026-08-29

Documentation

Fixed

Added

Changed

Fixed

Added

[0.1.0a2] - 2026-08-28

Added

Fixed

Six defects that shipped in 0.1.0a1. Together they meant a generated service could not be installed, could not be built into an image, could not answer a GET, and could not answer a PATCH. Each is now covered by a test, and by scripts/smoke_docker.sh, which builds the generated image and runs it against a real PostgreSQL -- the check whose absence let all six through.

Added

A pre-release is pinned exactly, because ~=0.1 does not match 0.1.0a1 either: a compatible-release clause normalises to >= 0.1, == 0.* and 0.1.0a1 sorts below 0.1.0, so it is out of range even with --pre. Once the framework reaches a final release the pin becomes ~=major.minor on its own.

A test now fails if any requirements template hardcodes a version again. The resolution itself is deliberately not checked in CI: at release time the version being pinned is not published yet, so that check would fail on exactly the commit that is correct.

Added

Fixed

Fixed

Added

Changed

Added

Changed

Fixed

[0.7.0] - 2026-08-28

Files, tenants, and the three cloud services a deployed app reaches for.

Added

storage plugin - Named disks with a visibility, modelled on Laravel's: code writes to storage.disk("private") and where that lives is configuration. - Local and S3/MinIO drivers behind one StorageBackend protocol. A local disk writes atomically (temp file + replace) so a reader never sees a partial object. - A private disk refuses to produce a permanent URL. temporary_url() signs the key and the expiry with HMAC, compared in constant time — signing only one of the two makes a single valid link a key to the whole disk. - Every key is validated before it reaches a filesystem or a bucket: traversal, absolute paths, backslashes and null bytes rejected, .. resolved first. Local disks re-check after resolution, because a symlink inside the root can still point outside it. - Downloads are Content-Disposition: attachment + nosniff. An uploaded .html or .svg served inline runs the uploader's script on your origin. - Expired and forged links return the same 403 with the same message. - MinIO in the generated compose file at port offset +6, opt-in.

tenancy plugin - Resolves the tenant from a token claim, a subdomain, a path prefix or a header, in that order of trust. header is not in the default list and warns in production: X-Tenant-ID: acme is one curl away from another tenant's data. - Subdomain parsing rejects multi-label hosts, the bare base domain, and a reserved list (www, api, admin, …). base_domain is required, or every hostname looks like a tenant. - require_tenant returns problem+json 403, with health, metrics and docs exempt so probes still pass. - jfast workspace caddy --wildcard-tenants emits a wildcard site block with on-demand TLS and the ask endpoint that gates it. Without ask, anyone pointing DNS at you can burn your certificate rate limit.

Social login (auth) - Google, Microsoft and GitHub presets; any other provider by its endpoints. - /auth/{provider}/start and /auth/{provider}/callback, with the state and nonce carried in an httponly, samesite=lax cookie and both verified on the way back. - ID tokens verified for audience and issuer. Without the audience check, a token minted for anyone else's Google app logs in here. - @auth.on_identity is where a verified identity becomes your user. Missing it is a 500, not a cheerful 200. - OIDCIdentity.federated_id is provider-qualified, because subject ids are unique per provider and not globally.

Secrets - load_secrets() populates os.environ from AWS Secrets Manager or Google Secret Manager before create_app(). An existing environment value wins unless overridden; only names are logged, never values; nested JSON is refused rather than given an unpredictable flattened name.

Serverless - jfast deploy function <name> --target aws|gcp writes the handler, the Dockerfile and a deploy script — and does not run them. - Both targets run the same ASGI app the container runs. Private by default on both clouds; --public opts in and warns.

notifications plugin - Firebase Cloud Messaging over the HTTP v1 API, with a console backend that logs instead of sending for development and tests. - Unregistered device tokens are reported back so they can be deleted. - Not verified against a real FCM project in CI; the payload construction is.

Fixed

Added (internal)

[0.6.0] - 2026-08-27

JWT authentication, and Kubernetes manifests derived from the service contract.

Added

auth plugin - Verification in three modes: jwks (fetch the issuer's public keys — the default, and the only sane one across services), public_key (a pinned PEM), secret (HMAC, for a single service). - require_auth, require_scopes(...), require_roles(...), optional_auth as FastAPI dependencies. 401 for "who are you", 403 for "you may not". - JWKS client with caching, rotation on an unknown kid, and a rate limit on refresh so forged kids cannot be used to hammer the identity provider. Cached keys keep working through a JWKS outage; /ready reports staleness. - Token issuance for a service that owns its own login, with refresh rotation and reuse detection: a replayed refresh token revokes the whole session family. - Revocation: POST /auth/logout denies the jti and its refresh family, backed by Redis when the cache plugin is on. The in-memory fallback reports itself as not shared rather than pretending. - GET /auth/me returns identity and permissions — never the token, never the raw claims.

Security decisions, each with a test - Algorithms are pinned by configuration and passed explicitly to the decoder, so alg: none and RS256→HS256 confusion are both refused. Configuring symmetric and asymmetric algorithms together is rejected at startup: that combination is the attack. - aud and iss are verified — off by default in most libraries, and without them a token for a sibling service is accepted here. - Expiry leeway is 30 seconds, not minutes. - Rejection reasons go to the log; the client gets a plain 401. - tenant_id now comes from a signed claim, not the forgeable X-Tenant-ID header. This is the main security reason to enable auth.

Kubernetes - jfast workspace k8s — a kustomize tree: Deployment, Service, ConfigMap, HPA and PodDisruptionBudget per service, one Ingress, dev/prod overlays. - jfast init asks whether you need it. - Liveness probes /health, readiness probes /ready — the two-endpoint contract is what keeps a database blip from restarting every healthy pod. A startup probe allows 150s for a slow first boot. - Non-root, read-only root filesystem, dropped capabilities, maxUnavailable: 0, and a PDB so a node drain cannot take every replica. - Ingress serves /api, the same shape as the generated Caddyfile, so the frontend build is identical locally and in the cluster.

Not generated, deliberately

Notes

[0.5.0] - 2026-08-27

Per-project contracts, and a quickstart that CI actually executes.

Added

Contracts - contracts.toml in every generated service: scope (owns / does_not_own), layer boundaries, forbidden calls, required structure, declared interfaces, and invariants no checker can verify. - jfast contracts init | check | show --json | render | waivers. check exits non-zero, so it fails a build rather than printing advice. - A static, AST-based checker: layer boundaries (relative and absolute imports), per-layer forbidden packages, forbidden calls with the reason attached, and required files per module. - Inline waivers — # contracts: allow <reason> — with the reason required and jfast contracts waivers listing every one. - The contract is validated before the code: two layers claiming one path, or a may_import naming a layer that does not exist, are reported as contract errors rather than producing confident answers to the wrong question. - CONTRACTS.md generated from the same file, so the document and the enforced rule cannot disagree. - .jfast/skills/respect-contracts/SKILL.md, and AGENTS.md now opens with jfast contracts show --json.

Getting started - docs/local-setup.md — installing from a checkout, generating a project, the loop you actually use, and the failure modes worth knowing. - scripts/smoke_docs.sh runs those commands exactly as documented, in CI. Documentation that has never been executed is a guess.

Fixed

[0.4.0] - 2026-08-27

Polyglot services, queues and events, one-command start, Caddy at the edge, and a documentation site — plus real build verification for everything that had until now only been verified by grep.

Added

jfast start - One command for the opinionated default: a Python modular monolith with PostgreSQL + pgvector, Redis, background jobs and a starter module, a Vue frontend, a Caddyfile and a workspace compose file. - A monolith rather than three services on purpose: splitting later is a move, un-splitting is a rewrite.

Polyglot services - docs/service-contract.md — the contract every JFast service satisfies regardless of language: /health, /ready, X-Request-ID, problem+json, JFAST_* config, ten-port blocks, JSON logs on stdout. - jfast new service --language go — a Go service with zero third-party dependencies, implementing the contract in ~300 vendored lines. CI runs go vet, go test, go build, starts the binary and curls it. - jfastframework/languages.py — the language registry. You only need the toolchain for the languages you actually use.

gRPC - --grpc generates the .proto contract (health, problem, a domain service) and reserves port offset +9. Contract only: no stubs are generated and no server is wired, because pinning a protoc version inside a scaffolder makes generated stubs disagree with whatever CI has. See proto/README.md.

Queues - queue plugin with a QueueBackend protocol and three backends: PostgreSQL (FOR UPDATE SKIP LOCKED, transactional enqueue), Redis (BLMOVE into a per-worker processing list), RabbitMQ (dead-letter exchange with a TTL for delays). - TaskRegistry and Worker: bounded exponential backoff, dead-lettering, job timeouts, in-flight draining on shutdown, immediate wake on stop. - GET /queue/stats.

Events - events plugin — Kafka publish/subscribe with partition keys, offsets committed after handling, and KRaft-mode infrastructure (no ZooKeeper).

Edge and workspace deployment - jfast workspace compose — one compose file for every service, whatever its language, with per-service datastores. - jfast workspace caddy — a Caddyfile putting the workspace behind one hostname. Backends live under /api with or without a gateway, so the frontend's production build keeps working the day one appears. - Frontends gained .env.production with VITE_API_URL=/api — relative, so no CORS and no rebuild per environment.

Documentation site - docs-site/build.py renders the repository's own markdown into a static, versioned site; docs-site/check.py validates links, anchors, assets, theme tokens and unrendered template artifacts. - .github/workflows/pages.yml publishes it, rebuilding every released minor version from its own tag so older versions keep working.

Fixed

Changed

Not done, deliberately

[0.3.0] - 2026-08-27

Multi-service workspaces, an API gateway, frontend generation, and the migration/testing setup that the previous release only documented.

Added

Workspaces - jfast.workspace.toml and jfastframework.workspace: services register themselves, take the next free ten-port block, and the file records what the frontend should call. - jfast workspace init | list | gateway | env.

API gateway - gateway plugin: prefix-based reverse proxy with hop-by-hop header stripping, X-Request-ID propagation, and 502/504 as problem+json. - Generated automatically once a workspace has more than one backend. One backend deliberately does not get one. - Not a catch-all: only configured prefixes are proxied, so the gateway keeps its own /health, /ready and /metrics. Readiness does not probe upstreams, so one restart does not fail the whole system.

Frontends - jfast new service <name> --kind spa --frontend vue|react — Vite + Tailwind v4 project with a working home page that calls the backend's /health on load. - jfast new view <Name> — the Modulo<Name>/{Components,Pages,Routes,Services} structure, registered in the router and the sidebar at marker comments. - jfastframework.cli.patcher: idempotent, loud, marker-preserving patching. Re-running the generator does not duplicate; a missing marker raises with the path instead of silently doing nothing. - Framework auto-detected from the project, so --frontend is not repeated.

Migrations and tests in generated services - alembic.ini, migrations/env.py, script.py.mako and versions/. env.py reads the app's own JFAST_DB_DSN and auto-imports every module's models, so autogenerate cannot silently emit an empty migration. compare_type and compare_server_default are on. - pytest.ini and a conftest.py with app / client fixtures.

Datastore selection from the terminal - jfast init — interactive installer: kind, frontend, datastores, port. - jfast new service --with database,cache,qdrant,rag — the plugin list, the [plugin.*] blocks, the .env keys and the pinned extras all derive from it. - rag's store is inferred from the datastores chosen, so --with qdrant,rag cannot generate a service configured for pgvector.

Changed

Fixed

[0.2.0] - 2026-08-27

Datastores became a choice, and a frontend became a service.

Added

Datastores - VectorStore protocol in jfastframework.vectors, with Chunk and SearchHit as the shared vocabulary. Every store normalises its score to cosine similarity in [0, 1]. - qdrant plugin — client, health check, container with HTTP and gRPC ports. - mongo plugin — Motor client and database handle. - rag now selects its store from config: pgvector, qdrant, or a dotted path to your own class. Same for the embedder. - InfraService.extra_ports for containers exposing more than one port.

Server-rendered frontends - web plugin — Jinja2 templates, static files, and render() with HTMX partial rendering: a browser navigation gets the page, an hx-get gets the fragment, from one handler. - HTMX-aware error handling: a JFastError raised during an HTMX request returns an HTML fragment instead of problem+json, which HTMX would otherwise swap into the DOM as raw text.

Generator - jfast new service <name> [--kind api|web] — scaffolds a whole service. - jfast new module <name> [--layout layered|screaming] [--ui api|htmx]. - module_screaming layout: framework-free domain, one file per use case, storage and HTTP at the edges, domain tests separated from use-case tests. - ui_htmx overlay — composed onto either layout rather than duplicated, so three template trees cover all four combinations. - Two Jinja environments in the scaffolder: .html.j2 templates use [[ ]] for scaffold-time values so the runtime {{ }} the browser needs survives. - Table names are pluralised, which also dodges the SQL reserved words that singular nouns keep landing on (order, user, group). Override with --table.

Docs - docs/modules.md, docs/datastores.md.

Changed

Fixed

[0.1.0] - 2026-08-27

First alpha. Kernel and built-in plugins.

Added

Notes