Skip to content

ufraaan/holo

Repository files navigation

holo

go Next.js TypeScript websocket MIT PRs Welcome


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.

how it works

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"]
Loading

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.

message protocol

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?

structure

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

scalability

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.

running locally

see CONTRIBUTING.md for detailed setup instructions, codebase conventions, and how to submit changes.

backend

cd backend
go mod tidy
go run ./cmd/holo-server

the server listens on http://localhost:8080. websocket endpoint: /ws.

requires Go to build and run.

frontend

cd frontend
bun install
bun run dev

the 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 dev

docker

docker compose up -d

the 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

try it live · report a bug · feature request

About

P2P inspired file sharing web app.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Contributors