Peer-to-peer encrypted file transfer. No accounts, no cloud, no trust required.
bore moves a file between two machines with a short human-readable rendezvous code. The default transfer path is direct peer-to-peer: bore discovers each peer's network address via STUN, exchanges candidates through a lightweight signaling server, and establishes a direct UDP connection via hole-punching. If the direct connection fails (e.g., both peers behind symmetric NATs), bore falls back to a relay automatically.
All file data is end-to-end encrypted with Noise XXpsk0 regardless of transport path. The relay is payload-blind -- it forwards encrypted bytes without any ability to inspect file contents.
The relay now serves a same-origin browser surface built with Astro + Vue from web/:
/is the Bore homepage served by the Go relay from the built web assets/ops/relayis a live read-only operator page backed by the relay's Go-owned/statusendpoint
The primary terminal operator surface now lives in tui/ as an OpenTUI console over that same /status contract.
v1.0.1 -- current stable release with relay and browser-surface hardening. Current truth:
Rewrite note: BUILD.md is the active execution manual for the v2 full rewrite onto Bun + TypeScript + Astro + Vue + Elysia + Zod + PostgreSQL + Docker Compose + Caddy. This README still describes the shipped v1 codebase that exists today, while the repo now also contains a real Phase 3-complete web-foundation v2 workspace under apps/, packages/, infra/, and db/.
- the repo is one Go module rooted at
github.com/dunamismax/bore - binaries live under
cmd/:bore,relay,bore-admin, andpunchthrough - shared Go packages live under
internal/:client,relay, andpunchthrough - the active browser surface lives in
web/(Astro + Vue on Bun), served same-origin bycmd/relay - the active terminal operator surface lives in
tui/(OpenTUI on Bun), pointed at the relay's Go-owned/statusendpoint - the v2 rewrite landing zone now exists at repo root as a Bun workspace with
apps/api,apps/web,packages/contracts,infra/caddy,docker-compose.yml, and.env.example - direct P2P is the default transfer path -- STUN discovery, signaling, hole-punching
- QUIC-based direct transport with production-quality congestion control (default)
- ICE-like multi-candidate gathering (host, server-reflexive candidates)
- connection quality metrics tracking (throughput, byte counters)
- relay is the automatic fallback when direct fails
- end-to-end encryption protects all data regardless of transport method
- resumable single-file transfers with on-disk checkpoint state
- the relay is hardened with per-IP rate limiting, HTTP timeouts, operational metrics, and deployment packaging
bore sendandbore receivewith direct P2P transport by default- automatic relay fallback when direct connection fails
--relay-onlyflag to force relay transport- transport method reporting (direct/relay + fallback reason)
- rendezvous code generation and parsing
- Noise
XXpsk0handshake bound to the rendezvous code - ChaCha20-Poly1305 encrypted transfer channel
- SHA-256 file integrity verification
- resumable single-file transfers with on-disk checkpoint state
- STUN/NAT discovery and relay-coordinated signaling for peer candidate exchange
- UDP hole-punching with QUIC transport (default) or reliable framing layer (fallback)
- multi-candidate gathering (host interfaces, STUN server-reflexive)
- self-hostable WebSocket relay with
/healthz,/status, and/metrics - per-IP rate limiting on relay
/wsand/signalendpoints - room ID validation on relay join/signaling paths, with signaling limited to live relay rooms
- explicit HTTP server timeouts (read, write, idle, header)
- Astro + Vue browser surface at
/and/ops/relay, served same-origin by the relay from built static assets - OpenTUI relay operator console in
tui/with live refresh, room gauges, direct-vs-relay summaries, and clear stale/error states bore-admin statusrelay polling as a compatibility shim- deployment packaging (Dockerfile, systemd service unit)
- standalone
punchthroughCLI for NAT probing
The rewrite is no longer doc-only. The repo now contains a verified Phase 3-complete web-foundation landing zone for the next-generation stack:
- root Bun workspace with shared
lint,check,test,build, andverifycommands apps/apiElysia service with typed env parsing, boot-time SQL migration application, structured JSON request logs, per-IP write-path rate limits, JSON body caps, request and idle timeout controls, plus/api/health,/api/readiness,/api/sessions,/api/sessions/:code,/api/sessions/:code/join, and/api/ops/summaryapps/webAstro + Vue app with typed send, receive, and ops shells for/,/send,/receive/[code], and/ops- a shared browser API client over
packages/contractsso web fetches stay schema-validated instead of ad hoc JSON parsing - Bun unit coverage for Vue SFC render shells, the typed web client, and composables, including client-side and API-surfaced validation states
- Playwright browser smoke coverage across desktop and mobile widths for the v2 send, receive, and ops shells
packages/contractswith shared Zod schemas for health, readiness, session, operator-summary, error payloads, and typed coordination envelopes for the upcoming realtime lanedb/migrationsplus checked-in Bun runners fordb:migrateanddb:resetinfra/caddy/Caddyfile,docker-compose.yml, and.env.exampleso the v2 lane runs ascaddy + api + postgres + web
This is still a build-phase scaffold, not a cutover claim. The shipped product remains the Go-first v1 described above.
| Component | Location | Status | Purpose |
|---|---|---|---|
bore client |
cmd/bore, internal/client/ |
active | P2P QUIC direct transport, relay fallback, crypto, transfer engine, CLI |
relay |
cmd/relay, internal/relay/ |
active | Signaling server for P2P connections, fallback transport, room broker |
web |
web/ |
active | Astro + Vue homepage and read-only relay operator surface |
tui |
tui/ |
active | OpenTUI relay operator console for live status, room gauges, and transport mix |
punchthrough |
cmd/punchthrough, internal/punchthrough/ |
active, integrated | NAT probing, STUN discovery, UDP hole-punching -> QUIC transport |
bore-admin |
cmd/bore-admin |
compatibility shim | Minimal relay status CLI kept for terse checks alongside the OpenTUI console |
Bore's relay path does not need a durable database today.
internal/relay/roomkeeps live room state in memory only.web/is a read-only browser surface that consumes the relay's live/statussnapshot.tui/is the primary terminal operator surface over that same/statusendpoint.bore-adminis a smaller stateless compatibility CLI over that same/statusendpoint.- resumable transfer checkpoint state is persisted as JSON on the receiver's filesystem (not in a database).
- transfer history and persisted operator history are not implemented yet.
For the shipped v1 codepath, keep relay state in memory unless a maintenance need clearly earns more. The planned v2 rewrite in BUILD.md moves durable application metadata and operator history to PostgreSQL.
The rewrite lane now boots independently from the shipped Go runtime.
bun install
bun run verify
docker compose up -d --buildIf port 8080 is already in use locally, override it for the v2 stack run:
BORE_V2_HTTP_PORT=18080 docker compose up -d --buildThe v2 stack maps PostgreSQL to host port 15432 by default so it does not collide with a local 5432 Postgres. Override it if you need a different host port:
BORE_V2_POSTGRES_PORT=25432 docker compose up -d --buildUseful API hardening knobs for the v2 lane live in .env.example:
BORE_V2_API_REQUEST_TIMEOUT_MSBORE_V2_API_IDLE_TIMEOUT_SECONDSBORE_V2_API_MAX_REQUEST_BODY_BYTESBORE_V2_API_RATE_LIMIT_WINDOW_MSBORE_V2_API_RATE_LIMIT_MAX_REQUESTS
Once the stack is up:
- v2 web shell through Caddy: http://127.0.0.1:8080/
- v2 ops shell through Caddy: http://127.0.0.1:8080/ops
- v2 health endpoint through Caddy: http://127.0.0.1:8080/api/health
- alternate port example when overriding locally: http://127.0.0.1:18080/api/health
The v2 lane is intentionally separate from the shipped Go relay runtime. It does not replace cmd/relay, web/, or tui/ yet.
go install github.com/dunamismax/bore/cmd/bore@latest
go install github.com/dunamismax/bore/cmd/relay@latestOr build from source:
git clone https://github.com/dunamismax/bore.git
cd bore
go build ./cmd/bore
go build ./cmd/relaygo build ./cmd/relay
go build ./cmd/borecd web
bun installcd web
bun run buildRELAY_ADDR=127.0.0.1:8080 go run ./cmd/relayThe relay serves as:
- Signaling server -- coordinates P2P candidate exchange between peers
- Fallback transport -- forwards encrypted bytes when direct P2P fails
- Same-origin browser surface -- serves the built Astro output from
web/dist
With the web build present:
- product page: http://127.0.0.1:8080/
- relay ops page: http://127.0.0.1:8080/ops/relay
- raw status JSON (relay): http://127.0.0.1:8080/status
For browser-only frontend iteration outside the shipped runtime path, you can also run:
cd web
bun run devcd tui
bun install
bun run start --relay http://127.0.0.1:8080go run ./cmd/bore-admin status --relay http://127.0.0.1:8080./bore send ./report.pdf --relay http://127.0.0.1:8080bore attempts a direct P2P connection by default. If direct fails, it falls back to the relay automatically.
Example output:
bore send -- report.pdf (58213 bytes)
Code: Ahcj7nQZclo-j15A_xGS8w-868-outer-crane-crane
Relay: http://127.0.0.1:8080
Waiting for receiver...
Sent: report.pdf (58213 bytes, 1 chunks)
SHA-256: a1b2c3...
Transport: transport=direct
./bore receive Ahcj7nQZclo-j15A_xGS8w-868-outer-crane-crane --relay http://127.0.0.1:8080./bore send ./report.pdf --relay http://127.0.0.1:8080 --relay-onlycd web
bun install
bun run lint
bun run check
bun testbun install
bun run lint
bun run check
bun run test
bun run build
bun run verify
docker compose up -d --build
# or: BORE_V2_HTTP_PORT=18080 docker compose up -d --build
# or: BORE_V2_POSTGRES_PORT=15432 docker compose up -d --buildBrowser smoke coverage for the current v2 web shells:
bun run --cwd apps/web test:browserBORE_V2_DATABASE_URL=postgres://bore:[email protected]:15432/bore_v2 bun run --cwd apps/api db:reset
BORE_V2_DATABASE_URL=postgres://bore:[email protected]:15432/bore_v2 bun run --cwd apps/api db:migrate
BORE_V2_DATABASE_TEST_URL=postgres://bore:[email protected]:15432/bore_v2 bun --cwd apps/api test tests/integration.test.tscd tui
bun install
bun run lint
bun run check
bun testgo test ./internal/client/... ./cmd/bore
go build ./cmd/borego test ./internal/relay/... ./cmd/relay
go build ./cmd/relaygo test ./internal/punchthrough/... ./cmd/punchthrough
go build ./cmd/punchthroughgo test ./cmd/bore-admin
go build ./cmd/bore-admin.
├── cmd/
│ ├── bore/
│ ├── bore-admin/
│ ├── punchthrough/
│ └── relay/
├── apps/
│ ├── api/
│ └── web/
├── db/
│ └── migrations/
├── internal/
│ ├── client/
│ │ ├── code/
│ │ ├── crypto/
│ │ ├── engine/
│ │ ├── rendezvous/
│ │ └── transport/
│ ├── punchthrough/
│ │ ├── punch/
│ │ └── stun/
│ ├── relay/
│ │ ├── metrics/
│ │ ├── ratelimit/
│ │ ├── room/
│ │ ├── transport/
│ │ └── webui/
│ └── roomid/
├── packages/
│ └── contracts/
├── infra/
│ └── caddy/
├── docker-compose.yml
├── package.json
├── bun.lock
├── web/
│ ├── src/
│ │ ├── components/
│ │ ├── layouts/
│ │ ├── lib/
│ │ ├── pages/
│ │ └── styles/
│ ├── tests/
│ └── package.json
├── tui/
│ ├── src/
│ │ └── lib/
│ ├── tests/
│ └── package.json
├── docs/
├── ARCHITECTURE.md
├── CHANGELOG.md
└── SECURITY.md
README.md- current product status, quick start, and verification commandsARCHITECTURE.md- system layout, transport layering, and design notesBUILD.md- active v2 rewrite plan and phase trackerSECURITY.md- threat model, implemented guardrails, and current limitsdocs/status-contract.md- Go-owned/statuscontract consumed by the browser surface,tui/, andbore-adminCHANGELOG.md- release history
┌────────────────────┐
│ Can peers connect │
│ directly? │
└────────┬───────────┘
│
┌─────────┴─────────┐
│ │
Yes (default) No
│ │
┌──────┴──────┐ ┌───────┴───────┐
│ direct path │ │ relay path │
│ STUN + UDP │ │ (automatic │
│ hole-punch │ │ fallback) │
│ -> QUIC │ │ │
└─────────────┘ └───────────────┘
Both paths use the same Noise XXpsk0 E2E encryption.
The relay never sees plaintext.
QUIC provides congestion control and flow management
for direct transport (~340 MB/s on loopback).
- TURN-style relay candidate in multi-candidate gathering
- directory transfer after single-file resume semantics are proven
- connection migration for mobile/roaming scenarios
- deeper operator tooling where it solves real relay problems
- The rendezvous code is a cryptographic input to the handshake, not just a routing token.
- The relay serves as both signaling server (P2P candidate exchange) and fallback transport.
- End-to-end encryption is identical regardless of transport path -- the relay is always payload-blind.
- Direct P2P is the default. Relay is the fallback. Use
--relay-onlyto force relay transport. - If docs and code disagree, the docs are stale. Fix both in the same change.