A self-hosted, team-based web IDE with a real terminal, per-project Docker containers, and an in-browser Node.js runtime for instant live previews. Built so humans and AI agents can share the same workspace through one consistent API.
Status: v1 — feature complete. Auth, projects, files, terminal, per-project containers, and WebContainer-powered live preview all work end-to-end. AI-agent and collab features are explicitly out of scope for v1 and tracked under v2 roadmap.
Browser (React + Monaco + xterm.js)
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
FastAPI REST FastAPI WebSocket WebContainer (in-browser
(auth, files, (xterm ↔ tmux/ Node.js, sandboxed,
projects, docker exec) no server resources used)
containers)
│ │
▼ ▼
PostgreSQL per-project Docker container
(users, teams, (mounted to /workspace,
projects) ports 9000-9099 for previews)
Two parallel execution paths run side-by-side:
npm install, npm run dev, and an iframed live preview all happen inside the user's tab. Used for the Lovable / Bolt-style instant-preview experience.Files live on a Docker volume and are shared between both runtimes. Edits in Monaco are reflected live in either path.
/auth/register, /auth/login).team_id.current_user dependency enforces ownership on every project-scoped route..git, .env) are filtered from the tree but reachable by direct path.Path.resolve() containment check.Ctrl+S to save the active one. Models are preserved per file path so cursor/scroll survive tab switches.docker exec (if the project container is running) or a tmux session in the backend (fallback).+ new / ✕ close / ↻ reset. Each tab is an independent tmux session, so a stuck Claude Code login can be killed without losing your other shells.Ctrl+F), Unicode 11, OSC 8 hyperlink support, WebGL renderer.coding-env-project-{id}./var/run/docker.sock mounted into the backend).node:22 / node:20 / bun / python:3.12 / python:3.11 / ubuntu./workspace — files are live, no copy step.:3000 is mapped to it. http://<your-host>:<port> is the live preview.@webcontainer/api boots a Node.js sandbox in the user's tab.package.json (skipping node_modules / dist / build / .git); user picks the project root from a dropdown.npm install / npm run dev etc. run real commands.POST /auth/register | /auth/login JWT
GET /teams | POST /teams Team CRUD
POST /teams/{id}/join Join existing team
POST /projects | GET /projects Project CRUD
GET /projects/{id} Project detail
GET /projects/runtimes Runtime list
GET /projects/{id}/files Tree
GET /projects/{id}/files/{path:path} Read (raw text)
PUT /projects/{id}/files/{path:path} Write
DELETE /projects/{id}/files/{path:path} Delete
POST /projects/{id}/files/move Move src→dst
POST /projects/{id}/exec One-shot command in container
POST /projects/{id}/container/start Start/resume project container
POST /projects/{id}/container/stop Stop (preserves state)
DELETE /projects/{id}/container Stop + remove
GET /projects/{id}/container Status
WS /ws/projects/{id}/terminal?token=&session=&rows=&cols=&use_docker=
Binary protocol: raw I/O frames; resize = 0x01 + 2-byte rows + 2-byte cols
git clone https://github.com/The-Code-Labz/coding-environment.git
cd coding-environment
cp .env.example .env # edit POSTGRES_PASSWORD, SECRET_KEY at minimum
docker compose up --build
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8000 |
| Swagger | http://localhost:8000/docs |
| Wiki | http://localhost:8000/wiki |
| Project previews | http://localhost:9000-9099 (allocated dynamically) |
First-time flow: Register → Create team → Create project (pick a runtime) → opens the editor.
| Variable | Default | What it does |
|---|---|---|
POSTGRES_PASSWORD | password | Postgres user password (interpolated into compose) |
DATABASE_URL | postgresql+asyncpg://postgres:${POSTGRES_PASSWORD}@postgres:5432/codingenv | Async DB URL |
SECRET_KEY | changeme-in-production | JWT signing — set this in prod |
ALGORITHM | HS256 | JWT alg |
ACCESS_TOKEN_EXPIRE_MINUTES | 60 | Token lifetime |
WORKSPACE_ROOT | /workspace | Where project files live (must match the volume mount in compose) |
CORS_ORIGINS | (empty) | Comma-separated allowed origins |
VITE_API_URL | http://localhost:8000 | Frontend → backend URL (build-time for Vite) |
The backend tolerates extra env vars (extra="ignore" on Settings), so adding deployment-specific vars to .env won't crash startup.
Depends(get_db) — it opens a short-lived AsyncSessionLocal() for the auth/project lookup, snapshots project.path, and releases the connection before entering the I/O loop. Without that, every open terminal pinned a pool slot and exhausted it after ~10 tabs (QueuePool limit … reached). See app/api/routes/terminal.py:307-372.pty.openpty()) so SIGWINCH propagates correctly; resize events update both the PTY and tmux resize-window.?rows=&cols=), so the backend's tmux session is sized correctly before any output prints. Critical for Claude Code's OAuth login URL not getting wrapped/truncated.package.json files exist; files are remapped before being fed to webcontainer.mount(). No surprise mounts of nested package roots.Cross-Origin-Embedder-Policy: credentialless so SharedArrayBuffer (needed by WebContainer) works without breaking external image hotlinks.filesApi.read uses responseType: "text" + a no-op transformResponse so axios doesn't auto-parse package.json into a JS object on the way back from the API. (Took an embarrassingly long time to track down — symptom was WebContainer mounting an empty package.json.)These all work well enough for v1 but are worth flagging:
apt install inside a container survives stop but not remove. No way to pin Node 18 or Deno without editing RUNTIMES in app/services/docker_manager.py.SQLAlchemy.metadata.create_all() on startup. Schema changes will need DROP TABLE or a manual ALTER until Alembic is wired up.http://host:9000+. No TLS, no per-project subdomain. Fine on localhost; ugly on a public VPS.node_modules and the running dev server. Re-boot from scratch every session.(c to copy) flow because we monitor the system clipboard, but other CLIs that overpaint URLs across redraws may still hit the same TUI-truncation issue.Goals are grouped by what unlocks what — not by phase number — so you can pick off whichever bucket has the most leverage at any time.
Problem: the generic-image model can't run a project that needs system deps or non-listed runtimes. Plan:
Dockerfile at project root → docker build on Start, use the built image.devcontainer.json (the VS Code spec) → use the same flow Codespaces / Coder do.package.json#engines.node and pick a matching prebaked image automatically.Problem: the API surface exists but agents aren't wired up. Plan:
agent_keys table — scoped, revocable, audit-logged./exec and file write attributed to a key with timestamp + diff.POST /projects/{id}/agent/run endpoint that takes {prompt, model} and runs Claude / GPT against the project's full context.Problem: real-time multi-user editing is a Phase 4 placeholder right now. Plan:
y-monaco binding, y-websocket provider on a new WS /ws/projects/{id}/doc/{path} endpoint.node_modules (harder; may need to lean on WC's built-in mount/export calls).<project-id>.preview.your-domain.com) instead of raw :9000+ — needs a Caddy/Traefik front in compose and DNS wildcard.monaco-languageclient package.These were flagged during the v1 wrap-up. They're small, but each one will bite once we start building on top, so they're worth doing before the bigger v2 buckets above.
metadata.create_all() startup hook with a real Alembic setup. First non-trivial schema change after agents land will otherwise require a DROP TABLE. ~1 evening of work./workspace/team-*/project-* dirs, and orphaned DB rows accumulate forever. Cascade must: stop+remove container, free preview port, rm -rf workspace dir, delete row.tmux capture-pane -p -J against the active session, regex-scan the joined output, surface in the same banner. Means the IDE sees the URL even if the user never copies it.coding-environment/
├── app/
│ ├── api/routes/ auth, teams, projects, files, terminal,
│ │ containers, exec, wiki
│ ├── core/ config (pydantic-settings), database (async SQLA),
│ │ security (JWT + bcrypt)
│ ├── models/ User, Team, Project (SQLAlchemy)
│ └── services/
│ └── docker_manager.py per-project container lifecycle
├── frontend/
│ └── src/
│ ├── pages/ Login, Dashboard, Editor
│ ├── components/ FileTree, TerminalsPanel, TerminalTab,
│ │ WebContainerPanel
│ ├── hooks/ useWebContainer
│ └── lib/api.ts axios client + typed endpoints
├── docker-compose.yml backend, frontend, postgres
├── Dockerfile backend image
└── .env.example every var explained
docker compose down # stop, keep data
docker compose down -v # stop + delete workspace and DB
docker compose up --build # rebuild after dep changes
MIT — see LICENSE.
Built by The-Code-Labz
Content type
Image
Digest
sha256:ce8092784…
Size
200.4 MB
Last updated
about 2 months ago
docker pull otakulabz/forge