JFastFramework
Genera el backend que nadie quiere escribir dos veces, y evita que se degrade mientras un equipo — o un agente — trabaja sobre él.
Un comando te da un servicio FastAPI con PostgreSQL, Redis, jobs en segundo plano, un frontend Vue y un reverse proxy, cableados entre sí y corriendo bajo Compose. Lo que escribes tú es la parte que solo tú conoces: las reglas de tu negocio. Lo que la mantiene coherente después es un contrato que CI hace cumplir.
Estado: alpha, en PyPI -- la versión actual es la que muestra PyPI; un número escrito aquí se queda viejo. La madurez se rastrea por subsistema en vez de con un solo número de versión: STATUS.md dice qué está probado contra infraestructura real, qué no está verificado y qué se sabe roto. Léelo antes de depender de cualquier parte.
jfastframework en PyPI · Documentación
Un comando
pip install jfastframework
jfast start shop
shop/ FastAPI · PostgreSQL · pgvector · Redis · jobs
shop-web/ Vue 3 · Vite · Tailwind
docker-compose.yml one container per resource
Caddyfile one hostname, TLS, static assets
.env every DSN, generated from the bindings
jfast dev # containers up, migrations applied, API and frontend running
¿Prefieres elegir? jfast init pregunta. ¿Prefieres flags? Cada elección es
una.
Para quién es esto
| Úsalo cuando | Porque |
|---|---|
| Arrancas backends seguido — trabajo de agencia, herramientas internas, un producto con varios servicios | La plomería se genera y se versiona, así que el décimo cuesta lo que costó el primero |
| Un equipo chico va a mantener lo que escribas | Los límites entre capas los verifica CI, así que una review es sobre la feature y no sobre dónde quedó el archivo |
| Estás dirigiendo agentes de IA por un codebase | Cada regla es legible por máquina y se hace cumplir, así que un agente que se desvía rompe el build en vez de mergear |
| Quieres empezar como monolito y separar después | Las fronteras entre módulos se verifican, así que las costuras siguen siendo reales; cuando a uno le queda chico el resto, jfast new service levanta su deploy y recablea el workspace |
Cuándo no usarlo
Acá importan más las respuestas honestas que una feature más.
| No lo uses cuando | Usa en su lugar |
|---|---|
| Estás escribiendo un endpoint, o un script con una UI web encima | FastAPI pelado. Esto es mucha estructura para un solo archivo |
| El equipo ya tiene un framework propio y convenciones que funcionan | El tuyo. Acá el valor son las opiniones, y ya tienes algunas |
| Necesitas el admin de Django, su ecosistema de ORM o su auth de fábrica | Django. Esto no intenta ser eso |
| Estás en un stack síncrono y no quieres async | Flask, o FastAPI sin esto |
| Necesitas hoy un store de usuarios con MFA, verificación de email y recuperación de contraseña | Todavía no — el plugin accounts tiene usuarios, login, roles y bloqueo, y ninguna de esas tres. STATUS.md es explícito al respecto |
Qué obtienes en realidad
shop/ one service
├── main.py routers register themselves here
├── jfast.toml which plugins are on; which layout each module uses
├── contracts.toml the rules, checked by CI
├── modules/
│ └── invoice/ una capacidad de negocio (modular, por defecto)
│ ├── api/ entra HTTP, sale respuesta
│ ├── services/ las reglas — sin SQL, sin Request
│ ├── repositories/ queries — sin conceptos de HTTP
│ ├── models/ tablas y esquemas
│ ├── validations/ reglas que necesitan la base
│ └── tests/
├── shared/ what two modules both need
└── migrations/
El framework es una librería que tu servicio importa, no código copiado dentro. Arreglas algo en el framework y todos los servicios lo reciben en el siguiente bump de versión. Lo que se genera es solo la parte que es genuinamente tuya.
Los casos de uso para los que fue construido
Estas son las formas que le calzan. Si la tuya no es una de ellas, la tabla de arriba es la guía honesta.
Una agencia que arranca un proyecto de cliente cada pocas semanas. El
octavo backend no debería costar lo que costó el primero. jfast start produce
el mismo stack siempre, así que el desarrollador que lo tome en un año
encuentra el layout que ya conoce, y un arreglo del framework llega a los ocho
por un bump de versión en vez de por ocho parches.
Una herramienta interna que va a sobrevivir a su autor. El contrato es la
entrega: jfast contracts show --json declara qué posee el servicio, qué no, y
qué límites se hacen cumplir. La siguiente persona no tiene que inferir la
arquitectura leyendo el código.
Un producto que empieza como un servicio y todavía no sabe dónde están sus costuras. Arrancas como monolito modular con límites de módulo reales — verificados, para que no se erosionen en silencio — y promueves un módulo a su propio servicio cuando la carga o el equipo lo pidan. Separar después es una jugada; volver a unir es un rewrite.
Un codebase donde los agentes escriben la mayor parte del código. Las
reglas son ejecutables. Un agente que consulta la base de datos desde un
router, importa un módulo dentro de otro, o bloquea el event loop falla en
jfast contracts check con el archivo, la línea y el arreglo. Esa es la
diferencia entre un agente que supervisas y uno que puedes dejar corriendo.
La arquitectura que obtienes
Un monolito modular, con las costuras visibles. Un deployable, varios módulos, y límites que se hacen cumplir en vez de acordarse:
- Un módulo nunca importa otro módulo. Lo que dos módulos necesitan se mueve a
shared/, y el checker nombra el archivo al que hay que moverlo. shared/nunca importa un módulo. Sin esa segunda regla,shared/se convierte en donde termina todo.- Nada en
shared/toca la base de datos. Dos módulos compartiendo un repository son dos módulos compartiendo una tabla.
Cada módulo elige su propia forma. Un catálogo son cuatro archivos; un
módulo de órdenes que tiene que ser testeable sin base de datos quiere puertos
y adaptadores. Cuatro layouts — layered, modular, screaming,
hexagonal — elegidos por módulo, y recordados en jfast.toml para que los
comandos posteriores sepan dónde va un archivo nuevo.
docs/modules.md tiene la pregunta que te dice cuál elegir.
Y una salida. Cuando a un módulo le queda chico el monolito,
jfast new service billing levanta su servicio y el workspace recablea los
puertos, los DSN y el gateway a su alrededor. Mover el código del módulo sigue
siendo tarea tuya — nada aquí lo extrae por ti. Lo que la herramienta te quita
es el trabajo de infraestructura, que es la parte que suele frenar a la gente.
No tienes que adivinar las costuras el día uno, que es justamente la razón para
empezar como monolito.
Qué problema resuelve esto
Los generadores de scaffolding copian código. Cada servicio generado se convierte en un fork congelado en el momento de la generación: arreglas un bug en el cliente HTTP compartido y lo parchas a mano en doce lugares.
JFast separa las dos preocupaciones que los generadores confunden:
Runtime (jfastframework) |
Una librería versionada que tus servicios importan. Arréglala una vez, sube el pin. |
Generador (CLI jfast) |
Emite solo el código que es genuinamente tuyo. |
La segunda mitad es donde la mayoría de las herramientas se detienen: la
estructura generada se degrada salvo que algo la sostenga. Por eso el
generador también emite un contracts.toml, y jfast contracts check rompe el
build cuando el código se desvía de él.
Todo lo que está por encima del kernel es un plugin.
[plugins]
enabled = ["observability", "metrics", "database", "cache", "queue"]
disabled = ["sentry"]
Borra "cache" y el cliente de Redis, su health check y su contenedor en el
compose generado desaparecen todos juntos. La infraestructura se deriva del
grafo de plugins, así que no puede desviarse de lo que la app realmente carga.
Plugins
| Plugin | Hace | Extra |
|---|---|---|
observability |
Logs JSON, correlación por request-id y tenant | — |
metrics |
Métricas RED de Prometheus, /metrics |
metrics |
database |
SQLAlchemy async, sesiones, cableado de Alembic | db |
cache |
Caché Redis, pub/sub | cache |
queue |
Jobs en segundo plano sobre PostgreSQL, Redis o RabbitMQ, y tareas recurrentes | queue |
events |
Publicación/suscripción con Kafka | kafka |
channels |
Canales pub/sub declarados sobre memoria, Redis o Kafka | — |
mongo |
MongoDB vía Motor | mongo |
qdrant |
Base de datos vectorial Qdrant | qdrant |
rag |
Recuperación sobre pgvector o Qdrant | rag |
web |
Renderizado parcial con Jinja2 + HTMX | web |
gateway |
Reverse proxy basado en prefijos | gateway |
http |
Llamadas a servicios hermanos: deadlines, reintentos, circuit breaker | http |
auth |
Verificación JWT, scopes, revocación, login social | auth |
accounts |
Usuarios, login con contraseña, bloqueo, roles y permisos | accounts |
outbox |
Jobs y eventos que se confirman con las filas de la request | db |
idempotency |
Idempotency-Key: un POST reintentado recibe la primera respuesta |
db |
ratelimit |
Token bucket en Redis, por subject, tenant o IP | cache |
websocket |
WebSockets autenticados con backplane Redis y registro de conexiones | server |
storage |
Archivos en discos locales, S3 o MinIO | storage |
tenancy |
Tenant desde un claim del token, subdominio o path | — |
notifications |
Push por Firebase Cloud Messaging | fcm |
mail |
Email con templates, encolado por defecto | mail |
sentry |
Reporte de errores y performance | sentry |
Los plugins de terceros se registran por el mismo entry-point group, así que nada de aquí es privilegiado.
No solo Python
Un servicio JFast no es "un servicio escrito con JFast" — es un servicio que
cumple el contrato: /health, /ready,
X-Request-ID, problem+json, config JFAST_*, un bloque de diez puertos,
logs JSON en stdout.
jfast new service edge --language go --grpc
Ese servicio Go tiene cero dependencias de terceros e implementa el contrato en ~300 líneas vendorizadas. El gateway rutea hacia él sin saber que es Go; el workspace le asigna sus puertos; Caddy lo pone al frente junto con todo lo demás — porque todos hablan con el contrato, no con el lenguaje.
CI corre go vet, go test, go build, arranca el binario y le hace curl. Un
scaffold que nadie corrió es un pasivo que parece una feature.
--grpc genera el contrato .proto. No genera stubs ni cablea un
servidor — mira proto/README.md
para saber por qué, y para los comandos con los que hacerlo tú mismo.
Colas y eventos
Dos cosas distintas, dos plugins:
[plugin.queue]
backend = "postgres" # or "redis", "rabbitmq"
@tasks.task("send_invoice_email")
async def send_invoice_email(payload: dict) -> None: ...
await queue.enqueue(Job(task="send_invoice_email", payload={"id": 7}))
Empieza con PostgreSQL: encolado a través del plugin outbox, un job comparte la
transacción que produjo el trabajo, así que un rollback se lleva el job con él. Redis compra latencia, RabbitMQ
compra ruteo. Cuál elegir, y por qué.
La entrega es at-least-once — los handlers tienen que ser idempotentes. Los reintentos están acotados y el backoff tiene tope; los jobs agotados van a la dead letter en vez de quedar en loop.
Los eventos son la otra mitad: events es Kafka, para "esto pasó" en vez de
"haz esto".
Frontends
Renderizado en servidor, sin paso de build:
jfast new service storefront --kind web
jfast new module product --ui htmx
SPA, Vue 3 o React con Vite y Tailwind v4:
jfast new service admin --kind spa --frontend vue
cd admin && jfast new view Facturas
jfast new view crea src/ModuloFacturas/{Components,Pages,Routes,Services} y
lo registra en el router y en el sidebar en sus comentarios marcadores — de
forma idempotente, fallando ruidosamente si un marcador ya no está.
Viene en dos looks. nexora, el de por defecto, es el diseño liquid-glass con
el que está hecho este sitio -- sidebar de vidrio, claro y oscuro, selector de
color, pantalla de login. --template classic es el sencillo. Las vistas nuevas
siguen el look con el que se generó el proyecto. Ve Frontends.
Ambos frontends se instalan y se buildean en CI. Ese job existe por un bug
real: el comentario marcador quedó dentro de un comentario de bloque, cuyo */
interno lo cerró antes de tiempo y dejó el router sintácticamente inválido.
Todos los greps pasaron. Solo vite build lo agarró.
Angular y React Native no se generan. Por qué.
Workspaces, gateway, Caddy
jfast workspace init cometax
jfast new service billing --with database,cache
jfast new service catalog --with qdrant,rag # a gateway appears here
jfast workspace compose && jfast workspace caddy
Los servicios se registran solos y toman el siguiente bloque libre de diez puertos. Al segundo backend se genera un gateway automáticamente — un solo backend deliberadamente no lo recibe, porque agregaría un salto y una superficie de caída a cambio de nada.
Caddy es el borde (TLS, HTTP/3, compresión, la SPA buildeada); el gateway es el
proxy de aplicación detrás de él. Los backends viven bajo /api en cualquier
caso, así que el build de producción del frontend sobrevive a la aparición de
un gateway.
docs/workspaces.md · docs/deploy.md
Migraciones y tests, ya cableados
Cada servicio generado trae alembic.ini, migrations/env.py, pytest.ini y
conftest.py. env.py lee el mismo JFAST_DB_DSN que lee la app — una
migración no puede correr contra una base distinta a la del servicio — e
importa automáticamente los modelos de cada módulo, así que autogenerate nunca
emite en silencio una migración vacía. Detalles y las
trampas.
Autenticación
[plugin.auth]
mode = "jwks" # jwks | public_key | secret
jwks_url = "https://id.example.com/.well-known/jwks.json"
issuer = "https://id.example.com/"
audience = "billing"
algorithms = ["RS256"]
@router.post("/invoices")
async def create(caller: Principal = Depends(require_scopes("invoices:write"))):
...
Verificación, scopes y roles, rotación de claves JWKS, rotación de refresh con detección de reuso, y revocación compartida entre réplicas a través de Redis.
Los defaults rechazan los ataques que no parecen fallas: alg: none, confusión
RS256→HS256 (configurar las dos familias de algoritmos a la vez se rechaza al
arrancar — esa combinación es el ataque), tokens emitidos para un servicio
hermano, y un clock skew generoso. Las razones del rechazo van al log; el
cliente recibe un 401 pelado.
El tenancy deja de ser falsificable. El tenant viene de un claim firmado, o
de las fuentes del plugin tenancy. Un header X-Tenant-ID suelto nunca es un
tenant: queda en el log como tenant_claimed, y solo se confía en él cuando
tenancy pone header entre sus fuentes.
auth no tiene store de usuarios propio. El plugin accounts lo es --
usuarios, /auth/login, bloqueo, roles y permisos -- y un servicio con su propia
tabla de usuarios llama a auth.issuer desde su propia ruta.
docs/auth.md, docs/accounts.md.
Kubernetes
jfast workspace k8s --host app.example.com
kubectl apply -k k8s/overlays/dev
jfast init pregunta si lo necesitas. Obtienes un árbol de kustomize:
Deployment, Service, ConfigMap, HPA y PodDisruptionBudget por servicio, un solo
Ingress sirviendo /api — la misma forma pública que el Caddyfile generado — y
overlays dev/prod.
Liveness sondea /health, readiness sondea /ready. Ese contrato de dos
endpoints es lo que evita que un parpadeo de la base de datos reinicie de golpe
todos los pods sanos.
Las bases de datos no se generan. Un StatefulSet de PostgreSQL salido de un scaffolder es la manera en que la gente pierde datos. Los manifiestos leen un DSN desde un Secret. docs/kubernetes.md.
Contratos: reglas de las que un agente no puede desviarse
AGENTS.md dice qué hacer. Un contrato dice qué está permitido, y algo lo
verifica — que es la diferencia entre una regla y una sugerencia.
Cada servicio generado trae un contracts.toml que es tuyo:
[project]
owns = "Invoices and payments."
does_not_own = "Customers. Ask the catalog service."
[layers.domain]
paths = ["modules/*/[!_]*.py"]
may_import = []
forbid_packages = ["fastapi", "sqlalchemy", "pydantic"]
[[rules.forbid_call]]
pattern = "os.getenv"
except_in = ["settings.py"]
why = "Configuration is typed. Add a field to a settings model."
jfast contracts check
modules/invoice/repository.py:1: layer-package: 'storage' must not import 'fastapi'
(Data access. No business rules.)
El mismo archivo activa la verificación del bug que nunca lanza excepción: una
llamada bloqueante dentro de un async def, que frena todas las demás requests
del worker y aparece como latencia en un lugar completamente distinto.
blocking_demo.py:14: async-blocking: requests.get() blocks the event loop inside async send()
Exit distinto de cero — en CI, un build roto. Un agente generando código a toda velocidad se desvía de la prosa; no se desvía de un check que falla.
Tres audiencias, un archivo: el build lo lee por check, un agente por jfast
contracts show --json, un revisor por el CONTRACTS.md generado. Los waivers
son inline y exigen una razón. docs/contracts.md.
Por qué los agentes de IA son audiencia de primera clase
jfast contracts show --json # the rules THIS project holds itself to
jfast describe --json # settings schema, plugin graph, providers, infra
jfast workspace list --json # services, ports, API base URL, needs_gateway
jfast doctor
Nada de grep. Además .jfast/skills/ — una carpeta por tarea con un SKILL.md
que declara cuándo usarla y los pasos exactos — y AGENTS.md, las
reglas que un agente tiene que seguir aquí.
Documentación
El sitio se construye desde estos mismos archivos: https://jfabrizzio5.github.io/JFastFramework/
| Documento | Contenido |
|---|---|
| docs/local-setup.md | Instalar desde un checkout y hacer tu primer proyecto |
| docs/dev.md | El ciclo local: contenedores, migraciones, API y frontend en un comando |
| docs/agents.md | Trabajar con agentes de IA: qué se hace cumplir, y qué no |
| docs/contracts.md | Reglas por proyecto, verificadas |
| docs/auth.md | JWT: modos, los ataques rechazados, revocación, login con Google |
| docs/accounts.md | Usuarios, login con contraseña, bloqueo, roles y permisos |
| docs/transactions.md | Cuándo se confirma, conflictos, bloqueos, reintentos, el outbox y las idempotency keys |
| docs/ratelimit.md | Un token bucket que no se filtra bajo carga |
| docs/websockets.md | Sockets entre workers, y qué no se entrega |
| docs/upgrading.md | Qué rompe al subir de versión, filtrado a lo que aplica a tu proyecto |
| docs/storage.md | Discos, URLs firmadas, S3 y MinIO |
| docs/multitenancy.md | Subdominios, orden de confianza, row-level security |
| docs/cloud.md | Gestores de secretos, funciones serverless, push |
| docs/kubernetes.md | Manifiestos, probes, qué no se genera |
| docs/service-contract.md | Qué tiene que hacer todo servicio, en cualquier lenguaje |
| docs/modules.md | Layouts de módulo, HTMX, tipos de servicio |
| docs/datastores.md | PostgreSQL, Redis, Mongo, Qdrant |
| docs/queues-and-events.md | Jobs, streams, backends |
| docs/frontend.md | HTMX, Vue, React, el generador de vistas |
| docs/workspaces.md | Muchos servicios, el gateway |
| docs/migrations-and-tests.md | Alembic, pytest |
| docs/plugins.md | Escribir un plugin |
| docs/deploy.md | Compose, Caddy, Dockerfile |
| docs/skills.md | Escribir una skill |
| ARCHITECTURE.md | Decisiones y sus costos |
| PLAN.md | Hecho, parcial, sin empezar |
Verificar
pytest # 397 framework tests
ruff check src tests docs-site && ruff format --check src tests docs-site
mypy src # strict
bash scripts/smoke.sh # both module layouts, HTMX, alembic, a booting service
bash scripts/smoke_contracts.sh # a generated service passes its own contract
bash scripts/smoke_auth_k8s.sh # auth guards routes; manifests parse
bash scripts/smoke_workspace.sh # workspace, gateway, view patching
bash scripts/smoke_start.sh # the default stack, end to end
bash scripts/smoke_docs.sh # the quickstart, run exactly as written
bash scripts/smoke_go.sh # go vet, test, build, run, curl (needs go)
bash scripts/smoke_frontend.sh # npm install + vite build (needs node)
python docs-site/build.py --version latest --output site/latest
python docs-site/check.py site/latest
Qué no está verificado
Dicho sin rodeos, porque un framework que exagera su cobertura es peor que uno que admite el hueco:
- RabbitMQ y Kafka están escritos contra APIs documentadas pero nunca se probaron de ida y vuelta contra brokers reales en CI.
- Multi-tenancy lo impone el row-level security de PostgreSQL solo con
[plugin.database] rls = true, una política por tabla y un rol que no sea superusuario. Sin las tres es el filtro del repositorio: una convención. - RAG trocea a ancho fijo y sin reranking.
- Angular, React Native, Laravel, .NET no se generan en absoluto.
Licencia
MIT.