A LAN-based real-time document collaboration system. A host runs the system as a Docker container; users on the same subnet open a URL in their browser and collaborate in real time.
CBNU Team H capstone project.
This README is a working document for development — it maps the repository so teammates and AI agents can find things. The user-facing README comes once the product is done.
Read AGENTS.md first. It is the single entry point: workflow, coding principles, doc routing, and team conventions.
Stack: TypeScript · Next.js (App Router) · Yorkie (CRDT) for real-time sync, document persistence, and version history, backed by MongoDB · host-held JSON files for the app's own state. Single package, single app process, pnpm. Decided in docs/SRS-ko.md §2.3.2 and docs/adr/002-persistence-on-yorkie-mongo.md.
| Path | Contents |
|---|---|
AGENTS.md |
Entry point — read first. Workflow, coding principles, doc routing. |
CLAUDE.md |
Imports AGENTS.md for Claude Code. |
ROADMAP.md |
Overall plan and milestones. |
docs/ |
Deliverable docs: requirements (SRS-ko.md), module design, UI wireframes, architecture decisions (ADRs). |
tasks/ |
Work in progress (active/) and finished work (archive/YYYY/MM/). See tasks/README.md. |
scripts/ |
The task index and archive helpers (pnpm tasks:index, pnpm tasks:archive), plus detect-host-ip.sh, which pnpm docker:up runs first to fill in HOST_LAN_IP. |
app/ |
Next.js App Router — pages, layouts, route handlers. |
lib/ |
Shared code that both the app and server-side processes import. |
server/ |
The custom server entry point and the WebSocket hub. |
public/ |
Static assets served as-is. |
instrumentation.ts |
Server startup hook — prints the host link and the guest join address. |
Dockerfile · docker-compose.yml |
The image the host runs, plus the self-hosted Yorkie server and its MongoDB store. |
skills/ |
Skills for Claude Code. Empty for now — add them as we find workflows worth packaging. |
Root config files (next.config.ts, tsconfig.json, eslint.config.mjs, postcss.config.mjs) are the real, live settings — not templates.
As the host would — one command, the way the product ships:
pnpm docker:upThis runs scripts/detect-host-ip.sh, which finds the host's LAN IP and
writes it to .env as HOST_LAN_IP, then docker compose up --build. Every later run reuses the same
.env, no retyping addresses. (pnpm start runs the server process directly, without the Yorkie
container behind it — the host wants the image.) Detection asks the routing table which interface
carries the default route and takes that one's address, rather than guessing from the address itself —
so it still picks the right adapter on a campus LAN that hands out 172.x addresses, where guessing
would mistake the real address for a Docker or WSL virtual one.
Linux and macOS only. On Windows that means run it from WSL, which is where Docker Desktop's backend lives anyway; from Git Bash the script says so and stops rather than guessing. Under WSL it also checks for mirrored networking (see below) — without it WSL sits on its own NAT and the detected address is WSL's rather than your LAN's, so the check warns and continues rather than blocking.
Prefer to do it by hand, or on a host with no default route to read? docker compose up --build still
works — copy .env.sample to .env and fill in HOST_LAN_IP yourself (ip -4 addr on
Linux, ipconfig getifaddr en0 on macOS, ipconfig on Windows), or pass it inline:
HOST_LAN_IP=192.168.0.14 docker compose up.
Startup prints two lines. The Host: link proves you are the host — opening it gives you a cookie
and drops the secret from the address bar. The secret stays valid until the container restarts, so
treat the line as a credential, not a used-up ticket. The Guest: line is what anyone else on the
same subnet types into their browser.
rmf-app | Host: http://localhost:3000/api/auth/host?secret=…
rmf-app | Guest: http://192.168.0.14:3000
Guests reach the Guest: link only if they're on the same subnet and that subnet doesn't apply
client/AP isolation (common on campus and guest Wi-Fi — it blocks devices from reaching each other
even on the same network). If a guest can't connect, that's the first thing to rule out, not the IP —
and it's not something any script here can detect or fix, since it's a router/AP setting outside the
host machine.
Windows hosts — mirrored networking: if HOST_LAN_IP prints correctly but guests still can't reach
it, Docker Desktop's WSL2 backend is likely only forwarding the port to 127.0.0.1, not the real network
adapter. pnpm docker:up detects this and prints the fix; by hand, add networkingMode=mirrored under
[wsl2] in %UserProfile%\.wslconfig, then run wsl --shutdown and restart Docker Desktop. Requires
Windows 11 22H2+; on older Windows, forward the port to the host's LAN IP yourself (e.g.
netsh interface portproxy). wsl --shutdown closes every WSL session, not just this project's, so
finish other WSL work first.
Restarting the container mints a new secret and invalidates every host session — that is the revoke
path (docs/design/api.md), not an accident.
While developing:
pnpm install
docker compose up -d yorkie # Yorkie on :8080 — realtime sync needs it
pnpm dev # http://localhost:3000, same two lines on stdout
pnpm lint
pnpm test # node:test, no framework
pnpm buildThree traps worth knowing before you hit them, all found the hard way:
- Run
pnpm buildlast, not beforepnpm dev. A production build leaves a.nextthe dev server cannot use, and it fails withCould not parse module '[project]/instrumentation.ts', file not foundfor a file that plainly exists.rm -rf .nextfixes it. The block above is in a safe order; reversing the last two lines is not. - Docker Compose must be 2.20 or newer.
docker-compose.ymlusesattach: falseon the mongo service to keep the host's terminal readable. Older Compose (2.13 was measured) refuses the whole file withservices.mongo Additional property attach is not allowed. pnpm devis reachable from this machine only — test other devices againstpnpm start. Next's dev server refuses/_next/*to any host butlocalhost, so a phone athttp://<LAN-IP>:3000gets the server-rendered HTML and no client JavaScript. Nothing errors on screen: the page draws, React never hydrates, and the join form quietly falls back to a plainGET /join?nickname=…&password=…that never attempts a login — which reads exactly like a wrong password. Build first (pnpm build && pnpm start, orpnpm docker:up) and the restriction is gone.
Documents live in Yorkie, which persists them to MongoDB — see docs/SRS-ko.md §2.3.2.
The app's own state is written as JSON under .data/ — chat history and the workspace's members,
so a nickname keeps its colour across a restart. Workspace metadata is still to come. Sessions are
deliberately not written: restarting the container signs everyone out, which is the revoke path.
It survives down and up --build on the app-data volume, the same way documents survive on
mongo-data. Both are named volumes, so docker compose down -v still wipes them — that is the
"start the session over" button, and it is the only thing that does.
- This repo is canonical. Discussion may happen elsewhere (e.g., Notion), but the source of truth is here; sync is one-way into this repo.
- Docs live with the code. A change and the doc describing it belong in the same commit.
- Commit prefixes and doc language: see
AGENTS.md§5. - Line endings: enforced by
.gitattributes(eol=lf). On first clone run the same command on Windows / macOS / Linux:
git config --global core.autocrlf input