share files and text between devices instantly. no accounts, no storage, just a room. available in 6 languages (en, es, fr, pt, de, hi).
create a room, share the 6-character code, and anything you drop in is relayed directly to the other side. no uploads to disk, no database, no sign-up.
a lightweight go server acts as a websocket relay. it never inspects, stores, or processes the data it forwards. it simply groups connections by room ID and broadcasts messages from one client to the others.
sender browser → websocket → go relay server → websocket → receiver browser
flowchart LR
subgraph Room["room (in-memory, websocket clients)"]
direction LR
SG["server (go)<br>relay"]
SB["sender browser<br>upload"]
RB["receiver browser<br>download"]
end
SB -- upload --> SG
SG -- relay --> RB
RB -- download --> FB["file blob"]
when you drop a file, the browser reads it in 64 KB slices, base64-encodes each slice, wraps it in a JSON message with metadata (file ID, offset, final flag), and sends it over the websocket. the server receives the complete frame and forwards the raw bytes to every other client in the room (sender excluded). the receiving browser decodes each chunk and accumulates them until the final chunk arrives, at which point it assembles them into a downloadable Blob.
text shares work the same way, but as single messages rather than chunked sequences.
all communication uses JSON messages over the websocket:
| type | purpose | payload |
|---|---|---|
file-meta |
announces a new file transfer | fileId, name, size, mime |
file-chunk |
carries one 64 KB chunk | fileId, chunk (base64), offset, final |
text |
sends a text share | text, customName? |
holo/
├── backend/
│ ├── cmd/holo-server/ # starts the server
│ └── internal/server/ # relay logic, rooms, and connections
│ ├── client.go # reads from and writes to each websocket
│ ├── room.go # keeps track of who's in a room and relays messages
│ └── hub.go # manages all rooms and cleans up old ones
├── frontend/
│ ├── app/
│ │ ├── layout.tsx # root layout, metadata, i18n provider
│ │ ├── (main)/page.tsx # home page with the video background
│ │ ├── (main)/terms/ # terms of service page
│ │ ├── (main)/privacy/ # privacy policy page
│ │ └── room/[roomId]/ # the room where you drop files and send text
│ ├── components/
│ │ ├── BackgroundVideo.tsx # fullscreen video that plays behind everything
│ │ ├── LocaleSwitcher.tsx # language dropdown
│ │ └── room/
│ │ ├── useRoomWebSocket.ts # handles the websocket connection
│ │ ├── room-utils.ts # splits files into chunks, formats sizes
│ │ ├── RoomHeader.tsx # shows the room code and connection status
│ │ ├── ConnectionToast.tsx # join/leave notifications
│ │ ├── FileDropZone.tsx # drag-and-drop area for files
│ │ ├── TextInputArea.tsx # where you type text to share
│ │ └── TransferList.tsx # list of incoming and outgoing transfers
│ ├── hooks/
│ │ └── useOnClickOutside.ts # generic click-outside handler
│ ├── i18n/
│ │ ├── routing.ts # next-intl routing config (6 locales)
│ │ └── request.ts # per-locale message loader
│ ├── messages/ # translation JSON files (en, es, fr, pt, de, hi)
│ ├── proxy.ts # Next.js 16 proxy (locale detection + WebSocket upgrade)
│ └── next.config.mjs # next-intl + Sentry plugin chain
├── docker-compose.yml # runs both services with one command
├── backend/
│ ├── Dockerfile # multi-stage go build → 16 mb image
│ └── .dockerignore
└── frontend/
├── Dockerfile # standalone next.js build → 316 mb image
└── .dockerignore
| backend (go) | frontend (Next.js + TypeScript) | |
|---|---|---|
| what it does | stateless websocket relay. never inspects or stores data | browser app that chunks files, sends/receives, and renders the UI with a 6-locale i18n layer |
| how it works | each connection runs two goroutines: readPump reads messages and pushes them to the room, writePump pulls from a buffered channel and writes to the socket | four pages: landing page (/) with video background and create/join UI, room page (/room/[roomId]) with file drop, text input, and transfer list, terms (/terms) and privacy (/privacy) |
| connections | gorilla/websocket with 64 KB buffers, 2 MB max frame size, ping/pong keepalive; room names validated against a profanity filter during handshake | browser websocket API with reconnection support and retry button; room names validated on the landing page before connecting |
| file flow | receives the full frame and forwards raw bytes to every other client in the room | splits files into 64 KB chunks using File.slice(), base64-encodes each, and sends as JSON messages (file-meta + file-chunk); receiver accumulates chunks into a Blob for download |
| memory | holds one chunk per connection at a time; slow consumers get disconnected | sender processes one chunk at a time; receiver holds all chunks until the final one arrives, then assembles |
| lifecycle | rooms auto-expire after 10 minutes of inactivity, garbage collector runs every minute | ephemeral. refresh the page and you start fresh |
the relay is i/o bound, not compute bound. it never touches disk, runs no queries, and keeps no state.
- ~900 clients per room before the join/leave broadcast storm starts a cascade
- 10,000+ concurrent connections across multiple rooms (tested, no failure)
- relay latency ~0.1ms at low room sizes
- could run on a raspberry pi - a gigabit ethernet port is the real bottleneck
see LOAD_TEST.md for the full breakdown.
see CONTRIBUTING.md for detailed setup instructions, codebase conventions, and how to submit changes.
cd backend
go mod tidy
go run ./cmd/holo-serverthe server listens on http://localhost:8080. websocket endpoint: /ws.
requires Go to build and run.
cd frontend
bun install
bun run devthe app runs on http://localhost:3000 by default.
requires Bun to build and run.
to point the frontend at a different server address:
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws bun run devdocker compose up -dthe frontend runs on http://localhost:3000, the websocket relay on http://localhost:8080.
| image | size |
|---|---|
holo-backend |
16 mb |
holo-frontend |
316 mb |
requires Docker to build and run.
to point the frontend at a different server address, pass it as a build arg:
docker compose build --build-arg NEXT_PUBLIC_WS_URL=wss://your-domain.com/ws frontend
docker compose up -d