{"slug": "freeleaf-an-free-and-open-latex-collaborative-suite", "title": "FreeLeaf: An free and open latex collaborative suite", "summary": "FreeLeaf has launched as a free, open-source, self-hostable collaborative LaTeX editing platform that runs entirely on the user's own infrastructure, with no third-party cloud or account required. The stack combines Django and Django Ninja for the API, React with Vite and TypeScript for the web app, a Node y-websocket service for Yjs collaboration, a sandboxed TeX Live compile worker, Postgres, MinIO and Mailpit, and starts with `docker compose up` across six services. Production deployment requires a separate explicit build of the `freeleaf-texlive:latest` image, which the standard `docker compose -f docker-compose.prod.yml up -d --build` command skips, causing compiles to fail until it is built on the same Docker host the compile service runs on.", "body_md": "An open and free, self-hostable, collaborative LaTeX editing platform. Self-host it on your own infrastructure for full digital sovereignty over your documents and data — no third-party cloud, no account with someone else's ToS standing between you and your own research.\n\nFull spec: [`Plan.md`](https://github.com/DeepMicroscopy/FreeLeaf/blob/main/Plan.md). Working conventions for contributors/agents: [`CLAUDE.md`](https://github.com/DeepMicroscopy/FreeLeaf/blob/main/CLAUDE.md).\n\nSee [`Status.md`](https://github.com/DeepMicroscopy/FreeLeaf/blob/main/Status.md) for current progress.\n\nDjango + Django Ninja (`apps/api`) · React + Vite + TS (` apps/web`) · Node/` y-websocket` (`apps/collab`) · sandboxed TeX Live (` apps/compile`) · Postgres · MinIO · Mailpit. See `Plan.md` §4.\n\nRequires Docker and Docker Compose.\n\n```\ndocker compose up\n```\n\nThis starts:\n\n| Service | URL | \n|---|---|\n| web (React SPA) | [http://localhost:5173](http://localhost:5173) | \n| api (Django + Ninja) | [http://localhost:8000/api/health](http://localhost:8000/api/health) | \n| collab (Yjs sync) | ws://localhost:1234 (health: [http://localhost:1234/health](http://localhost:1234/health) ) | \n| compile (sandboxed TeX Live runner) | [http://localhost:8100/health](http://localhost:8100/health) | \n| Mailpit (dev inbox for magic links) | [http://localhost:8025](http://localhost:8025) | \n| MinIO console | [http://localhost:9001](http://localhost:9001) | \n\nA fresh clone should build and start with no extra setup.\n\nAlways open the app at **[http://localhost:5173](http://localhost:5173)**, not `http://127.0.0.1:5173`. Browsers treat the two as different sites, so cookies from the api (at `localhost:8000`) won't reliably carry over if you mix them — this bites hardest in Safari.\n\n```\ncp .env.prod.example .env.prod\n# fill in .env.prod: real secrets, your domain, Mailgun, ORCID production app\ndocker compose -f docker-compose.prod.yml --env-file .env.prod up -d --build\ndocker compose -f docker-compose.prod.yml build texlive\n```\n\nThis builds production images (gunicorn + collected/whitenoise-served static for the api, an nginx-served Vite build for the web) and runs a single public-facing `web` container that reverse-proxies `/api`, `/admin`, `/static`, and `/collab` to the internal services — everything is same-origin in production, which avoids the CORS/cross-site-cookie fragility dev's separate `:5173`/`:8000` origins have. Only `web` is meant to be internet-facing; terminate TLS in front of it (a managed load balancer, or your own Caddy/nginx with certs proxying to `web`'s port) — `apps/web/nginx.conf` only serves plain HTTP.\n\n**The second command is not optional.** `texlive` (the sandboxed compile worker image, built from `docker/texlive/Dockerfile`) is deliberately excluded from the profile the first command builds — it's never run as a long-lived container, only spawned per compile job by `compile` via the Docker Engine API, referencing the image by a plain local tag (`freeleaf-texlive:latest`, no registry). `up -d --build` skips it entirely, so compiles will fail with no image found until you build it explicitly, on the same Docker host `compile` runs on. Re-run this build any time `docker/texlive/Dockerfile` changes — nothing rebuilds it for you automatically.\n\n`docker-compose.prod.yml` uses `name: freeleaf-prod` so it never shares containers/volumes with a dev stack running from the same checkout; its example port (`8001`) is chosen so both can run side by side during testing — map to `80`/` 443` for a real deployment.\n\n**Running the web app and api on separate subdomains instead** (your own reverse proxy, not `apps/web/nginx.conf`) works too, but `DJANGO_ALLOWED_HOSTS`, `FRONTEND_URL`, `CORS_ALLOWED_ORIGINS`, and `CSRF_TRUSTED_ORIGINS` stop being the same value — see the comment block at the top of `.env.prod.example` for exactly which one needs which host. Getting `DJANGO_ALLOWED_HOSTS` wrong here surfaces as a 400 with nothing useful in the browser; check `docker compose -f docker-compose.prod.yml logs api` for `DisallowedHost` first. **If you take this route, your own reverse proxy also needs a read/proxy timeout comfortably above 100s** — a large document (big images, custom fonts) can legitimately take well over a minute to compile, and `apps/web/nginx.conf`'s own generous timeout for `/api` never applies here since traffic to the api host bypasses it entirely. A compile that fails right around the one-minute mark with a bare 500 and CORS errors in the browser console (CORS headers never got attached because the request died before Django could build a response) is this timeout being too short, not an actual CORS misconfiguration — see `gunicorn`'s `--timeout 100` in `docker-compose.prod.yml` for the timeout budget the rest of the chain is sized against.\n\n**api**\n\n```\ncd apps/api\npython3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt\n.venv/bin/python manage.py migrate\n.venv/bin/python manage.py runserver\n```\n\n**web**\n\n```\npnpm install\npnpm --filter web dev\n```\n\n- api: `cd apps/api && python manage.py <cmd>` · test`pytest -q` · lint`ruff check .`\n- web: `pnpm --filter web <script>` · lint`pnpm lint` · typecheck`pnpm typecheck`\n\n`python3 scripts/make_promo_video.py` renders a silent, screenshot-driven promo video (for\nsharing on LinkedIn, etc.) from the screenshots in `docs/assets/img` — crossfaded scenes with\nanimated on-screen text and a soft procedural music bed (no narration, no bundled audio).\nRequires only `ffmpeg`. `--dry-run` prints the scene plan without rendering.\n\nCompilation of untrusted user LaTeX is sandboxed per `Plan.md` §7 — no shell-escape, no network, ephemeral containers, resource limits. See `docs/security.md` (added in Phase 3).", "url": "https://wpnews.pro/news/freeleaf-an-free-and-open-latex-collaborative-suite", "canonical_source": "https://github.com/DeepMicroscopy/FreeLeaf", "published_at": "2026-10-19 06:59:22+00:00", "updated_at": "2026-10-19 07:15:21.336544+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["FreeLeaf", "Django", "Django Ninja", "React", "Vite", "Yjs", "TeX Live", "MinIO"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/freeleaf-an-free-and-open-latex-collaborative-suite", "markdown": "https://wpnews.pro/news/freeleaf-an-free-and-open-latex-collaborative-suite.md", "text": "https://wpnews.pro/news/freeleaf-an-free-and-open-latex-collaborative-suite.txt", "jsonld": "https://wpnews.pro/news/freeleaf-an-free-and-open-latex-collaborative-suite.jsonld"}}