Hybrid Stack
The hybrid stack combines a Python FastAPI backend with a TypeScript Vite SPA frontend in a single turbo workspace and a single deployable container. The Python service serves the API, admin pages (Jinja2 + HTMX), and the Vite-built SPA — all from one process, one domain, one SSL cert.
Why
Separate frontend and backend deployments mean CORS config, two containers, two health checks, two CI pipelines. The hybrid approach collapses this into one:
- Same-origin API calls, no CORS.
- One container, one deploy, one health check.
- Server-rendered admin pages coexist with the SPA.
- Static file performance is fine — FastAPI’s
StaticFilesmiddleware handles it.
Workspace layout
The Python and TypeScript code live in the same turbo workspace under web/py/:
web/py/
apps/
py-app/ # FastAPI application
src/ # Python source
static/ # ← Vite output builds here
templates/ # Jinja2 templates (admin, emails)
ts-web-app/ # Vite React SPA source
packages/
py-core/ # Shared Python library (db, AI, events)
ts-core/ # TypeScript types mirroring py-core
turbo.json # Orchestrates both Python and TS tasks
pnpm-workspace.yaml # pnpm workspace (includes ts-web-app)
pyproject.toml # uv workspace (Python packages)
Turbo orchestrates both languages. pnpm dev (via make dev) starts the FastAPI server, taskiq worker, taskiq scheduler, and the Vite dev server in parallel. On build, the Vite SPA compiles into apps/py-app/static/ so the Python Dockerfile can bundle everything.
Route priority
1. /api/* → FastAPI API endpoints
2. /_status/* → Health checks
3. /admin/* → Jinja2 + HTMX server-rendered pages
4. /static/* → Vite-built assets (JS, CSS, images)
5. /* → SPA catch-all (serves index.html)
The catch-all lets React Router handle client-side paths. API routes are registered first so they always win.
Shared types
packages/ts-core/ mirrors the types from py-core. The Python models are the source of truth — ts-core manually defines matching TypeScript interfaces and keeps them in sync. This avoids code generation tooling and keeps both sides readable.
Real-time
SSE is the default for real-time. The Python service publishes events to per-user Redis channels via EventPublisher. The SSE endpoint subscribes and delivers them to the browser. HTMX connects via sse-connect, the React SPA uses EventSource. Both work same-origin with no proxy config.
Docker build
The Dockerfile at web/py/Dockerfile bundles both Python and the pre-built static assets. The Vite build must complete before the Docker image is created — turbo handles this dependency. The resulting container runs a single process serving the API, admin pages, and SPA.