Sign inSign up

otakulabz/forge

By otakulabz

•Updated about 2 months ago

Image
0

3.1K

otakulabz/forge repository overview

⁠⟨/⟩ Coding-Env

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⁠.


⁠What it does

                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:

  1. Server-side container (one per project) — full Linux box you can SSH-style attach to, run any toolchain in (Node, Bun, Python, Ubuntu). Long-running, stateful, port-mapped for live preview.
  2. WebContainer (in-browser) — StackBlitz's WASM-based Node.js runtime. Costs zero server CPU. Boot, 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.


⁠What's in v1

⁠Auth & multi-tenancy
  • Email + password registration, JWT-signed sessions (/auth/register, /auth/login).
  • Teams own projects; all data access is scoped by team_id.
  • current_user dependency enforces ownership on every project-scoped route.
⁠File management
  • File tree with create / rename / delete / move (with multi-select drag-and-drop).
  • Hidden files (.git, .env) are filtered from the tree but reachable by direct path.
  • Paths are sanitized against traversal via Path.resolve() containment check.
  • Multi-tab Monaco editor — open as many files as you want, dirty state per tab, Ctrl+S to save the active one. Models are preserved per file path so cursor/scroll survive tab switches.
⁠Terminal
  • WebSocket-bridged xterm.js ↔ either docker exec (if the project container is running) or a tmux session in the backend (fallback).
  • Multi-tab terminals with + new / ✕ close / ↻ reset. Each tab is an independent tmux session, so a stuck Claude Code login can be killed without losing your other shells.
  • Persistent across browser refreshes (tmux), search (Ctrl+F), Unicode 11, OSC 8 hyperlink support, WebGL renderer.
  • OAuth URL detection — when Claude Code / GitHub / Google print a login URL the IDE pulls it out of the scrollback and surfaces a "↗ Open login" button so OSC-8 truncation issues stop being a problem.
⁠Per-project Docker container
  • One container per project, named coding-env-project-{id}.
  • Talks to the host's Docker daemon via socket passthrough (/var/run/docker.sock mounted into the backend).
  • Runtime picker chooses from a fixed set: node:22 / node:20 / bun / python:3.12 / python:3.11 / ubuntu.
  • Project workspace is bind-mounted at /workspace — files are live, no copy step.
  • Host port allocated from 9000-9099; container's :3000 is mapped to it. http://<your-host>:<port> is the live preview.
  • Resource cap of 512 MB / 1 CPU per container.
  • Generic image + volume model — no Dockerfile-per-project support yet (see v2).
⁠WebContainer (in-browser runtime)
  • StackBlitz @webcontainer/api boots a Node.js sandbox in the user's tab.
  • Auto-detects every directory containing a package.json (skipping node_modules / dist / build / .git); user picks the project root from a dropdown.
  • Fresh-fetch policy at boot: clean files come from disk via the Files API, dirty tabs use in-memory content. No stale-cache bugs.
  • Output piped into the active terminal tab; npm install / npm run dev etc. run real commands.
  • Device-viewport preview — Mobile (375), Tablet (768), Laptop (1280), or Full. Centered iframe with soft shadow, smooth width transition, dotted backdrop. Same UX Bolt and Lovable have.
⁠Other UX
  • Optimistic file-tree mutations + 4-second polling reconciliation — file create/delete feels instant.
  • File tree refreshes on window focus.
  • Drag-and-drop multi-file move (Ctrl/Cmd-click + Shift-click range select).
⁠Backend service surface
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

⁠Quickstart

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

First-time flow: Register → Create team → Create project (pick a runtime) → opens the editor.


⁠Configuration

VariableDefaultWhat it does
POSTGRES_PASSWORDpasswordPostgres user password (interpolated into compose)
DATABASE_URLpostgresql+asyncpg://postgres:${POSTGRES_PASSWORD}@postgres:5432/codingenvAsync DB URL
SECRET_KEYchangeme-in-productionJWT signing — set this in prod
ALGORITHMHS256JWT alg
ACCESS_TOKEN_EXPIRE_MINUTES60Token lifetime
WORKSPACE_ROOT/workspaceWhere project files live (must match the volume mount in compose)
CORS_ORIGINS(empty)Comma-separated allowed origins
VITE_API_URLhttp://localhost:8000Frontend → 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.


⁠Architecture notes worth knowing

  • DB connection pool: terminal WebSocket handler does not use 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 model: tmux fallback uses a real PTY pair (pty.openpty()) so SIGWINCH propagates correctly; resize events update both the PTY and tmux resize-window.
  • Initial terminal dimensions: passed in the WebSocket query string (?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.
  • WebContainer mount root: the user picks the project root from a dropdown when multiple package.json files exist; files are remapped before being fed to webcontainer.mount(). No surprise mounts of nested package roots.
  • CORS / COEP: Vite dev server runs with Cross-Origin-Embedder-Policy: credentialless so SharedArrayBuffer (needed by WebContainer) works without breaking external image hotlinks.
  • File reads: 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.)

⁠Known limitations / sharp edges

These all work well enough for v1 but are worth flagging:

  • No Dockerfile per project. Runtime is one of 6 prebaked images. 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.
  • No DB migrations. Backend uses SQLAlchemy.metadata.create_all() on startup. Schema changes will need DROP TABLE or a manual ALTER until Alembic is wired up.
  • No project delete / rename UI (no DELETE endpoint either — orphaned containers and workspace folders pile up).
  • Single-team UX assumption. A user can belong to multiple teams (the model supports it), but the dashboard navigation flows assume one. Switching teams is awkward.
  • No tests. Zero test coverage anywhere — backend or frontend.
  • Plaintext preview URLs. Per-container previews are served as raw http://host:9000+. No TLS, no per-project subdomain. Fine on localhost; ugly on a public VPS.
  • WebContainer has no persistence. Closing the IDE tab loses node_modules and the running dev server. Re-boot from scratch every session.
  • Docker socket passthrough = root. The backend can do anything to the host's Docker daemon. Acceptable for self-hosted single-user; do not multi-tenant this without rootless Docker or a sandboxed daemon-in-daemon setup.
  • Resource caps are global, not per-team. Every container gets 512 MB / 1 CPU regardless of who owns it. No per-team quota.
  • OAuth URL detection is fragile. It works for Claude Code's (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.

⁠v2 roadmap

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.

⁠1. Reproducible projects (highest leverage)

Problem: the generic-image model can't run a project that needs system deps or non-listed runtimes. Plan:

  • Detect Dockerfile at project root → docker build on Start, use the built image.
  • Detect devcontainer.json (the VS Code spec) → use the same flow Codespaces / Coder do.
  • Cache built images by content hash so re-Start is fast.
  • Fall back to the runtime-picker behavior when neither file exists.
  • Bonus: detect package.json#engines.node and pick a matching prebaked image automatically.
⁠2. AI agents (the original product thesis)

Problem: the API surface exists but agents aren't wired up. Plan:

  • Per-project / per-user API keys with a separate agent_keys table — scoped, revocable, audit-logged.
  • Audit log table: every /exec and file write attributed to a key with timestamp + diff.
  • A POST /projects/{id}/agent/run endpoint that takes {prompt, model} and runs Claude / GPT against the project's full context.
  • Claude/Anthropic + OpenAI client wrappers; Bedrock optional.
  • Cost-tracking column on the audit table (input tokens, output tokens, $).
  • Agent UI: separate "Agents" tab that shows recent runs, lets you replay or fork them.
⁠3. Collaborative editing

Problem: real-time multi-user editing is a Phase 4 placeholder right now. Plan:

  • Yjs CRDT for Monaco — y-monaco binding, y-websocket provider on a new WS /ws/projects/{id}/doc/{path} endpoint.
  • Live cursors with user color from the JWT subject.
  • Shared terminal: broadcast tmux pane I/O to N WebSockets instead of one. (tmux already supports multi-attach — just need to fan out.)
  • Presence in the file tree (who's editing what right now).
⁠4. Persistence + reliability
  • Alembic migrations so the schema can evolve without nuking the DB.
  • WebContainer state save/restore — serialize the file tree (already easy) and the dev server's node_modules (harder; may need to lean on WC's built-in mount/export calls).
  • Terminal scrollback persistence — stash the last N KB of each tmux session in Redis so a reconnect re-renders the existing scrollback, not just "Connected".
  • Project delete + rename with proper cascade (kill container, free port, remove workspace dir, remove DB row).
⁠5. Production-ready deploy
  • Per-project subdomain previews (<project-id>.preview.your-domain.com) instead of raw :9000+ — needs a Caddy/Traefik front in compose and DNS wildcard.
  • TLS everywhere with auto-cert (Caddy is the path of least resistance).
  • Per-team resource quotas: max containers, max preview ports, total memory, total disk.
  • Rootless Docker or a sidecar daemon scoped per team — current socket passthrough makes the backend root-equivalent.
  • Observability: Prometheus metrics for active terminals, container starts, agent runs, exec endpoints. A small ops dashboard.
  • Tests. Backend: pytest + httpx for API + a docker-in-docker fixture for container lifecycle tests. Frontend: Playwright for the editor + terminal + preview happy paths.
⁠6. Editor polish
  • Monaco LSP integration so Python / TypeScript / etc. get real diagnostics, not just syntax highlighting. Run language servers inside the project container; bridge to Monaco via the monaco-languageclient package.
  • Find & Replace across files (currently it's per-file via Monaco's built-in find).
  • Git status + diff in the file tree (modified/new badges).
  • Per-tab split view (open the same file in two columns; or two files side-by-side).
  • Settings persistence — user theme, font size, tab width — currently always defaults.
⁠7. Other things worth doing
  • Project templates (Vite+React, Next, FastAPI, Astro). Currently every project starts as an empty workspace.
  • Snippet sharing — share a project as a read-only link with the live preview embedded.
  • Marketplace of starter Dockerfiles once #1 lands.
  • One-click deploy hooks — push a project's container to Fly / Railway / Render via their APIs.

⁠v1.1 — immediate cleanup (do these before v2 features land)

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.

  • Alembic migrations. Replace the 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.
  • Project delete + rename endpoint. No DELETE route currently exists — orphaned containers, orphaned /workspace/team-*/project-* dirs, and orphaned DB rows accumulate forever. Cascade must: stop+remove container, free preview port, rm -rf workspace dir, delete row.
  • Multi-team UX commitment. The model supports a user belonging to N teams, the dashboard assumes 1. Either build the team-switcher UI properly, or simplify the schema to one-team-per-user and remove the join endpoint. Pick one — current state is the worst of both.
  • Basic API tests. Pytest + httpx for the auth → create team → create project → file CRUD → container start happy path. Plus a docker-in-docker fixture so the container tests aren't skipped in CI. Lack of tests is the single biggest risk to landing v2 cleanly.
  • Robust OAuth URL extraction. Current detection is clipboard-poll-based — works for Claude Code's "press c to copy" affordance, but any CLI that prints a URL without that affordance still hits TUI truncation. Better fix: periodic 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.

⁠Project layout

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

⁠Stopping & resetting

docker compose down              # stop, keep data
docker compose down -v           # stop + delete workspace and DB
docker compose up --build        # rebuild after dep changes

⁠License

MIT — see LICENSE.


Built by The-Code-Labz⁠

Tag summary

Content type

Image

Digest

sha256:ce8092784…

Size

200.4 MB

Last updated

about 2 months ago

docker pull otakulabz/forge