A production-grade, non-Jac twin of the this is jac showcase. The original is a single fullstack Jac application (object-spatial graphs, walkers, client + server + native in one language). This repo rebuilds the exact same product with a conventional, boring, battle-tested stack — React + FastAPI + Postgres + Redis
- a C→wasm game — so you can compare the two side by side.
Everything the Jac app does (a public guestbook graph, an embedded littleX social network, a live graph visualizer, a "read this site's own source" viewer, and a C cube-shooter compiled to WebAssembly) is reproduced here with separate, independently deployable services.
┌──────────────────────────────────────────┐
browser ──:8080──▶ │ frontend (nginx : alpine) │
│ • serves the built Vite/React SPA │
│ • SPA fallback for /, /littlex, ... │
│ • reverse-proxies same-origin: │
│ /api/ /graph /docs │
│ /openapi.json /static/ ──┐ │
└─────────────────────────────────┼─────────┘
│
▼
┌──────────────────────────────────────────┐
│ backend (gunicorn + 4 uvicorn workers) │
│ FastAPI + async SQLAlchemy │
│ /api/* /graph /docs /static/* │
└───────┬───────────────┬─────────┬─────────┘
│ │ │
┌──────────▼───┐ ┌───────▼────┐ │ POST /analyze
│ postgres 16 │ │ redis 7 │ ▼
│ (durable) │ │ (cache) │ ┌──────────────────┐
└──────────────┘ └────────────┘ │ analytics │
│ (uvicorn x2) │
│ separate FastAPI│
└──────────────────┘
C cube-shooter ──(clang --target=wasm32)──▶ backend/static/main.wasm
served at /static/main.wasm,
instantiated by the WebGL shim.
| Service | Tech | Role |
|---|---|---|
frontend |
Vite + React + TS, served by nginx | SPA + single-origin reverse proxy to the backend |
backend |
FastAPI, async SQLAlchemy, gunicorn | The data API, graph visualizer, Swagger docs, static assets |
analytics |
FastAPI, uvicorn | A genuinely separate microservice (mirrors the Jac sv import) |
postgres |
PostgreSQL 16 | Durable relational store for all tables |
redis |
Redis 7 | Read-through cache (transparent in-process LRU fallback) |
| C → wasm | clang --target=wasm32 |
The cube-shooter game module (/static/main.wasm) |
The Jac version expresses the whole product in one object-spatial language. Here each Jac concept is reimplemented with its conventional equivalent:
| Jac feature | Non-Jac equivalent here |
|---|---|
Per-user root node |
A roots row (anonymous session) / a users + profiles row |
| Object-spatial graph (nodes + edges) | Relational tables with foreign keys (app/models/*.py) |
node / edge archetypes |
SQLAlchemy ORM models |
walker traversals |
FastAPI endpoints + SQLAlchemy queries (app/api/*.py) |
visit [-->] graph walk |
WHERE / JOIN over the FK relationships |
allroots() cross-root read |
An unscoped SELECT (no root_id filter) |
| Per-root read | WHERE root_id = :me |
sv import to another server |
A real HTTP call to the analytics microservice (POST /analyze) |
Jac client (.cl.jac) UI |
React + TypeScript components (frontend/src/) |
sv endpoints (RPC over the graph) |
REST endpoints under /api/* |
| Jac auth / per-user isolation | JWT bearer tokens + password_hash (app/core/security.py) |
Jac runtime graph visualizer (/graph) |
A server-rendered HTML graph page at /graph |
Native Jac (na) → wasm |
A hand-written C shooter compiled to wasm (native/shooter.c) |
| "Read this site's own source" panel | Pygments-highlighted source endpoints (/api/source/*) |
Zero setup — the backend falls back to SQLite and an in-process cache, so you don't even need Postgres or Redis running.
# one terminal: launches analytics (:8200), backend (:8100), frontend (:5173)
./scripts/dev.shThen open http://localhost:5173. The Vite dev server proxies /api,
/graph, /docs, /openapi.json, and /static to the backend, so everything
is same-origin (cookies, iframes, and wasm fetches all just work).
URLs:
| What | URL |
|---|---|
| App (open this) | http://localhost:5173 |
| Backend health | http://localhost:8100/api/health |
| Swagger docs | http://localhost:8100/docs |
| Graph visualizer | http://localhost:8100/graph |
| Analytics health | http://localhost:8200/health |
Build just the frontend bundle:
./scripts/build.sh # -> frontend/distcp .env.example .env # then edit: set TINJ_SECRET_KEY + POSTGRES_PASSWORD
docker compose up --buildOpen http://localhost:8080. The frontend nginx container serves the built
SPA and proxies the API to the backend service, which talks to postgres,
redis, and analytics.
Run database migrations (the production schema path):
docker compose exec backend alembic upgrade headThe app's
init_db()create_allruns automatically on startup for the zero-setup dev path; Alembic is the production migration path. Both produce the identical schema (seebackend/alembic/versions/0001_initial.py).
All backend config is read from TINJ_-prefixed env vars (see
backend/app/core/config.py). The most important ones:
| Variable | Example | Notes |
|---|---|---|
TINJ_ENVIRONMENT |
production |
development | production |
TINJ_DATABASE_URL |
postgresql+asyncpg://tinj:pw@postgres:5432/tinj |
Async DSN. Defaults to local SQLite. |
TINJ_REDIS_URL |
redis://redis:6379/0 |
Cache; falls back to in-process LRU if unreachable |
TINJ_SECRET_KEY |
<openssl rand -hex 32> |
Required in prod. Signs JWTs. |
TINJ_ANALYTICS_URL |
http://analytics:8000 |
Analytics microservice base URL |
TINJ_CORS_ORIGINS |
["http://localhost:8080"] |
Allowed browser origins |
TINJ_SOURCE_ROOT |
/repo |
Repo root the source viewer exposes (RO mount) |
TINJ_STATIC_DIR |
/app/static |
Bundled wasm / captures / logos |
TINJ_FRONTEND_DIST |
/app/frontend-dist |
Optional: let the backend serve the SPA directly |
Compose-only secrets (.env): POSTGRES_DB, POSTGRES_USER,
POSTGRES_PASSWORD. See .env.example for the documented full list.
The backend mounts these routers (full interactive list at /docs):
Meta
GET /api/health— liveness + cache backend + pid
Auth (/api/auth)
POST /register,POST /login,POST /logout,GET /me— JWT bearer auth
Guestbook (/api/guestbook)
POST /sign,GET "",GET /explore,GET /stats
Graph
GET /api/graph/data— graph snapshot (JSON)GET /graph— server-rendered live graph visualizer (HTML)
Source viewer (/api/source)
GET /files,GET /file?path=...— Pygments-highlighted source
littleX (/api/lx)
- profiles:
POST /setup_profile,GET /profile,GET /profiles - feed:
POST /feed,GET /trending,POST /tweet,POST /like,POST /comment,POST /delete - social:
POST /follow,POST /unfollow - channels:
GET|POST /channels,GET /channels/{id},POST /channels/{id}/join|leave|post
Analytics microservice (separate service)
GET /health,POST /analyze
This stack is built to scale horizontally:
- Gunicorn + uvicorn workers — the backend runs
gunicorn -w 4 -k uvicorn.workers.UvicornWorker. Tune-wto roughly2 × cores. Run multiple backend replicas behind a load balancer; they are interchangeable. - Stateless auth (JWT) — auth is a signed
HS256bearer token, so any backend replica can validate any request with no shared session store. Scale the API tier freely. - Redis cache — read-heavy endpoints (source listing, graph snapshots) are cached through Redis with a short TTL. The cache layer transparently falls back to an in-process LRU if Redis is unreachable, so a Redis blip degrades rather than breaks. Point all replicas at the same Redis for a shared cache.
- Postgres connection pool — async SQLAlchemy uses a real pool
(
db_pool_size,db_max_overflow,pool_pre_ping,pool_recycle). Size the pool soreplicas × (pool_size + max_overflow)stays under Postgresmax_connections; add PgBouncer in front for large fan-outs. - Separate analytics service — independently scalable. It is stateless and
can be replicated on its own schedule, exactly like the Jac
sv importtarget. - CDN for static / wasm —
frontend/distassets are content-hashed and served withCache-Control: immutable. Put a CDN in front of/assets/and/static/(includingmain.wasm) to offload bandwidth; the nginx config already sets long-lived caching headers for hashed assets. - Database migrations — schema changes ship via
alembic upgrade headas a pre-deploy step; the app never alters schema in production.
.
├── backend/ FastAPI app (app.main:app), models, alembic migrations
│ ├── app/ api/ core/ models/ services/ schemas
│ ├── alembic/ env.py + versions/0001_initial.py (all tables)
│ ├── static/ bundled wasm / captures / logos
│ ├── Dockerfile gunicorn + uvicorn workers, non-root, healthcheck
│ └── requirements.txt
├── analytics/ separate FastAPI microservice (app.main:app)
│ ├── app/main.py
│ └── Dockerfile uvicorn --workers 2, non-root, healthcheck
├── frontend/ Vite + React + TS SPA
│ ├── src/ components, littlex, wasm shim
│ ├── nginx.conf SPA fallback + reverse proxy (baked into the image)
│ └── Dockerfile bun build -> nginx:alpine
├── infra/nginx/ canonical nginx.conf (mirrors frontend/nginx.conf)
├── native/ C cube-shooter -> wasm (shooter.c, build.sh)
├── scripts/ dev.sh (local stack), build.sh (frontend bundle)
├── docker-compose.yml full production-like stack
├── .env.example documented env vars
└── README.md