A lightweight web-based file browser and code editor
  • Go 51.4%
  • TypeScript 41.5%
  • CSS 6.4%
  • Dockerfile 0.4%
  • HTML 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
w33ble 9a843adaec
All checks were successful
test / server (pull_request) Successful in 1m58s
test / client (pull_request) Successful in 17s
docker / docker (push) Successful in 2m56s
docs: require feature branches, no direct commits to main
2026-09-27 18:01:51 -06:00
.forgejo/workflows ci: build and push multi-arch docker image on merge to main (#4) 2026-09-02 11:39:59 -06:00
client feat(client): add favicon pack and manifest links 2026-09-27 17:59:29 -06:00
server feat: bound and stream file reads and writes 2026-08-22 15:30:55 -06:00
.gitignore chore: add package-lock.json, ignore tsbuildinfo 2026-06-01 19:18:08 -06:00
AGENTS.md docs: require feature branches, no direct commits to main 2026-09-27 18:01:51 -06:00
Dockerfile fix: run as 1000:1000 by default 2026-09-02 11:17:43 -06:00
Dockerfile.ci ci: build and push multi-arch docker image on merge to main (#4) 2026-09-02 11:39:59 -06:00
mise.toml chore: use latest go, add mise build-docker task 2026-09-02 11:17:43 -06:00
README.md feat: bound and stream file reads and writes 2026-08-22 15:30:55 -06:00

Pipit

A lightweight web-based file browser and code editor. Browse and edit files through a familiar split-pane IDE interface — file tree on the left, editor on the right.

Go backend, React frontend. Monaco editor with syntax highlighting. Live-reloads when files change on disk.

Quick Start

docker pull git.w33ble.com/w33ble/pipit:latest
docker run -p 8080:8080 -v /path/to/your/project:/workspace git.w33ble.com/w33ble/pipit:latest

Open http://localhost:8080.

Development

Prerequisites: Go 1.25+, Bun

# Terminal 1 — backend
cd server
go run .

# Terminal 2 — frontend (with hot reload)
cd client
bun install
bun run dev

The Vite dev server proxies /api requests to the Go backend at http://localhost:8080, so open http://localhost:5173.

Configuration

All settings via environment variables with a PIPIT_ prefix. Everything has a default.

Variable Default Description
PIPIT_PORT 8080 Server listen port
PIPIT_ROOT_DIR . (current directory) Root directory to serve files from
PIPIT_AUTH_PASSWORD (empty) If set, requires HTTP Basic auth on every request
PIPIT_CORS_ORIGIN (empty) Allowed CORS origin. Empty means same-origin only
PIPIT_MAX_FILE_SIZE 32MB Largest file that may be opened or saved. 0 disables the limit

Auth

When PIPIT_AUTH_PASSWORD is set, pipit uses HTTP Basic authentication. The browser prompts for credentials on first load and then sends them automatically with every request, including the live-reload event stream.

The username is ignored — there is one shared password, so enter anything you like as the user:

docker run -p 8080:8080 -e PIPIT_AUTH_PASSWORD=hunter2 \
  -v /path/to/your/project:/workspace git.w33ble.com/w33ble/pipit:latest

curl -u pipit:hunter2 http://localhost:8080/api/tree?path=/

When PIPIT_AUTH_PASSWORD is empty (default), no authentication is required.

Everything is behind the password, including the web UI itself — an unauthenticated visitor gets a prompt and nothing else. GET /api/health is the one exception, so container healthchecks keep working.

Basic auth sends the password on every request, encoded but not encrypted. Over plain HTTP anyone who can watch the traffic can read it. Treat it as a way to keep a casual second party out of a file browser on a network you already trust, not as a security boundary — pipit is not built to face the public internet.

File size

Pipit is meant to stay small enough to leave running on a server, so a single read or write is capped by PIPIT_MAX_FILE_SIZE (default 32MB, accepts a plain byte count or a KB/MB/GB suffix). Opening or saving something larger returns 413 with an explanation rather than loading it into memory. Set it to 0 to remove the limit.

Saves stream to a temporary file next to the target and are renamed into place, so memory use does not scale with file size and an interrupted save leaves the previous contents intact.

API

All paths are relative to PIPIT_ROOT_DIR. Path traversal (../) is rejected.

Method Endpoint Description
GET /api/health Health check
GET /api/tree?path=/ List directory contents
GET /api/file?path=/foo.ts Read file contents
PUT /api/file?path=/foo.ts Save file (body = raw content)
POST /api/file?path=/new.ts Create empty file
DELETE /api/file?path=/foo.ts Delete file
POST /api/dir?path=/newdir Create directory
DELETE /api/dir?path=/olddir Delete directory (must be empty)
POST /api/rename Rename file/directory (JSON: {"oldPath","newPath"})
GET /api/events SSE stream of file change events

SSE Events

The /api/events endpoint streams real-time file changes. Events are JSON:

{"type":"created","path":"src/new.ts"}
{"type":"modified","path":"src/app.tsx"}
{"type":"deleted","path":"src/old.ts"}

Tech Stack

Backend: Go standard library (net/http routing, os.Root sandboxed file access) + fsnotify for file watching

Frontend:

  • React 19 + TypeScript
  • TanStack Router (typed context)
  • TanStack Query v5 (server state)
  • Monaco Editor (@monaco-editor/react)
  • react-resizable-panels (split pane layout)

Everything: Tailwind-free, no CSS framework. Dark theme using CSS custom properties.

Project Structure

server/          Go backend
  main.go        Entry point, middleware stack, routes
  internal/
    config/      Env var parsing (PIPIT_ prefix)
    handler/     REST + SSE endpoints
    middleware/  CORS, auth, logging, path safety
    watcher/     fsnotify with debounce dedup

client/          React frontend (Vite)
  src/
    components/  FileTree, EditorTabs, MonacoEditor, Toolbar, LoginGate, Toast
    hooks/       useFileTree, useFileContent, useFileOps, useOpenTabs, useSSE
    lib/         API client, language detection

Testing

# Backend
cd server && go test ./... -v -count=1

# Frontend
cd client && bun run test