The specification, still in flight, and reference implementation of
Workspace's peer-to-peer data and permissions layer: a .workspace
portable bundle format, a workspace:// URI scheme for discovery, and
the permissions layer that sits underneath.
It began as a research spike (this repository was workspace-p2p-spike)
asking whether Hypercore would do. The answer was yes; the work since
has been designing and implementing the layers above it. Some of it is
still exploration, and says so (the documents marked as spikes).
A .workspace is a folder (or an archive of one) that contains a
collection of files plus the metadata needed to sync them and enforce
access between peers. The folder is the workspace. Sharing is a
file-system operation: copy it to a USB stick, AirDrop it, drop it on
a NAS, attach it to an email.
What's on disk is already safe to share — public files appear in
plaintext, tier-gated content lives only as encrypted bytes inside
.workspace/, and per-recipient bootstrap envelopes are sealed to
their intended DIDs. Copy-paste sharing does not leak. No "export"
step is needed for safety.
The same workspace, addressable by URL. Encodes the bootstrap metadata (workspace ID, root DID, Hyperswarm topic, attestation) compactly enough to embed in a chat message, a QR code, or a webpage. Tap it on any platform that has the Workspace app installed; the app opens, validates the attestation, asks the user to confirm, joins the swarm.
This is the magnet-link analogue: tiny, URL-shareable, leaks nothing sensitive, the actual content lives in the swarm.
Together, the folder form and the URI form are the two ways a workspace gets distributed and discovered. They carry the same underlying metadata; one is a file, one is a URL; both lead to the same place.
See docs/format/workspace-format.md for the
full format spec.
Three escalating ways to see this working, smallest first.
git clone https://github.com/workspace-sh/workspace-protocol.git
cd workspace-protocol
npm install
npm run typecheck
npm run testAll tests pass in-process — cryptographic primitives, UCAN delegation chains, bundle round-trip, and Hypercore replication through a direct duplex pipe. No network, no external dependencies.
npm -w @workspace.sh/p2p-spike-node run demo:acmeThree "Acme" members — Alice, Bob, Carol — share a workspace. Alice
creates a .workspace/ bundle with envelopes for Bob and Carol; the
data log carries content sealed under K0_org. Bob and Carol unwrap
their envelopes and decrypt. Eve (no envelope, no key) cannot. Runs
in one process via direct pipes — ~20ms end-to-end, deterministic,
doesn't touch the network.
For exercising the actual Hyperswarm transport without touching the host network:
# One-time: install a container runtime (Colima is fully FOSS)
brew install colima docker
# Per session: start the runtime
colima start
# Per run: build + run the demo. Brings up the bootstrap first
# (via compose dependency), runs acme against it, auto-exits when
# acme finishes.
docker compose up --build --abort-on-container-exit acme
# Per session: stop the runtime when you're done with all containers
colima stopThe bootstrap and peer process each run in their own container on a
private bridge network. Nothing leaves the bridge. Hard memory + CPU
caps protect against runaway processes. See
docker/README.md for the runtime options —
Colima recommended (fully FOSS); OrbStack and Docker Desktop work
identically with the same compose file.
The app codes against @workspace.sh/workspace — one object over the
proven primitives:
import { createRuntime } from '@workspace.sh/p2p-runtime/node';
import { Workspace } from '@workspace.sh/workspace';
const ws = await Workspace.create({ createRuntime, folder: 'Acme.workspace', name: 'Acme' });
await ws.invite(bobDid);
await ws.write(new TextEncoder().encode('hello'));
ws.on('change', render);
// on another device, after receiving the folder:
const ws2 = await Workspace.open({ createRuntime, folder: 'Acme.workspace', identitySeed });npm -w @workspace.sh/p2p-spike-node run demo:workspace runs this
end-to-end over a private swarm — the same flow as the hand-wired
acme-org-live demo, in ~10 lines.
npm -w @workspace.sh/p2p-spike-node run smoke— verifies the live Hyperswarm DHT works (needs internet)npm -w @workspace.sh/p2p-spike-node run demo:end-to-end— bundle + topic + replication over the live public DHTnpm -w @workspace.sh/p2p-spike-node run demo:bootstrap+demo:acme:live— same as the container demo but on the bare hostnpm -w @workspace.sh/p2p-spike-node run demo:key-delivery/demo:topic-auth— the #9 / #10 flows in isolation
See apps/node/README.md for the full
testing-mode matrix.
npm-only — Bun doesn't compose cleanly with react-native-macos.
| Node runtime — Hypercore + Hyperswarm | done |
| IPC layer — JSON-RPC over stdio | done |
| macOS TurboModule — NSTask spawning | done |
| Permissions layer — wrap primitive | implemented |
| Permissions layer — UCAN delegation boundary | implemented |
| Permissions layer — root attestation | implemented |
| Permissions layer — bootstrap envelopes | implemented |
Permissions layer — transparent log encryption (encryptedLog) |
implemented |
Workspace SDK facade (@workspace.sh/workspace) |
v1 implemented (single-writer; Autobase + doc model pending) |
| Live key delivery log (#9) | implemented (polish pending) |
| Topic-layer authentication (#10) | implemented (topic rotation pending) |
workspace:// URI scheme + .workspace format + discovery |
designed (specs locked); see docs/ |
| Autobase wrapper (#11) | pending |
.workspace folder + archive shape (#24) |
designed; implementation pending |
DNS / .well-known discovery (#25) |
designed; implementation pending |
| Layered peer discovery (local / LAN / WAN) | designed; only WAN implemented |
| Lighthouse — trusted always-on node | concept; post-spike, separate repo |
| Mobile path (#6) | separate workstream |
Permissions epic: #5.
Active development on feat/permissions-layer
(PR #22). 65 tests green across three packages; typecheck clean.
┌─────────────────────────────────────────────────────────────────────┐
│ .workspace folder / workspace:// URI │ ← Portable / discoverable
├─────────────────────────────────────────────────────────────────────┤
│ Bootstrap envelopes + bundled manifest + root attestation │ ← @workspace.sh/portable-bootstrap
├─────────────────────────────────────────────────────────────────────┤
│ UCAN 1.0 delegations (issue, validate, serialise) via iso-ucan │ ← @workspace.sh/ucan-boundary
├─────────────────────────────────────────────────────────────────────┤
│ Wrap primitive + root attestation + did:key encode/decode │ ← @workspace.sh/p2p-runtime (crypto)
├─────────────────────────────────────────────────────────────────────┤
│ Hypercore logs + Hyperswarm topics + NSTask IPC │ ← @workspace.sh/p2p-runtime (sync)
└─────────────────────────────────────────────────────────────────────┘
Each layer composes on the one below. Files in .workspace/ are
self-contained at rest; the URI form is a pointer the app resolves.
The cryptographic substrate is the same regardless of how the
workspace was distributed.
.
├── README.md ← you are here
├── FINDINGS.md ← spike outcomes + extraction checklist
├── docs/
│ ├── workspace-format.md ← .workspace on-disk format spec
│ ├── uri-scheme.md ← workspace:// URI scheme
│ ├── discovery.md ← DNS TXT + .well-known/workspace
│ ├── discovery-layers.md ← local-first / LAN / WAN peer discovery
│ ├── lighthouse.md ← trusted always-on node concept
│ ├── identity-recovery.md ← device linking, recovery, key rotation
│ ├── permissions-model.md ← UCAN + Hypercore protocol design
│ ├── threat-model.md ← what Workspace protects (and doesn't)
│ ├── risks.md ← where this could fail and what we're doing
│ ├── ucan-prior-research.md ← UCAN library notes from earlier spike
│ └── adr/ ← architecture decision records
│ ├── 0001-ucan-library.md ← superseded: UCAN 1.0 via iso-ucan
│ └── 0002-autobase-merge-strategy.md ← per-format LWW over Autobase
├── packages/
│ ├── p2p-runtime/ ← @workspace.sh/p2p-runtime
│ │ └── src/
│ │ ├── types.ts ← P2PRuntime, Log, Did, TopicId, LogKey
│ │ ├── runtime.{node,ios,android,macos,web,windows}.ts
│ │ ├── did.ts ← did:key encode/decode (ed25519)
│ │ ├── wrap.ts ← X25519 ECDH sealed envelopes
│ │ └── attestation.ts ← root attestation sign/verify
│ ├── ucan-boundary/ ← @workspace.sh/ucan-boundary
│ │ └── src/index.ts ← every UCAN library call lives here
│ ├── portable-bootstrap/ ← @workspace.sh/portable-bootstrap
│ │ └── src/
│ │ ├── index.ts ← createBundle / consumeBundle / createEnvelope
│ │ ├── folder.ts ← writeBundleFolder / readBundleFolder
│ │ ├── key-delivery.ts ← publishDelivery / scanDeliveries (#9)
│ │ └── membership.ts ← verifyMembership (#10)
│ └── workspace/ ← @workspace.sh/workspace (app-facing SDK)
│ └── src/index.ts ← Workspace.create / open / invite / write
└── apps/
├── node/ ← smoke harness over real Hyperswarm
├── macos/native/ ← P2PRuntimeModule (Obj-C++ TurboModule)
└── macos-probe/ ← Swift probe validating the NSTask path
The design lives in docs/, one folder per subject,
each with its own index:
- Format: the
.workspaceon-disk shape and theworkspace://URI scheme - Sync: blocks, merging, multi-writer, many workspaces in one runtime
- Network: discovery and network conditions
- Lighthouse: always-on peers a workspace opts into
- Permissions and identity: UCAN, joining, recovery, prior art
- Security: the threat model and risks
- Workspace actions: sandboxed functions in documents and workspaces (a spike within the spec)
- Decisions: architecture decision records
Start with format/workspace-format.md
for what's being built, then security/threat-model.md
for the contract that bounds it, format/uri-scheme.md
for addressing, permissions/permissions-model.md
for the protocol underneath, and security/risks.md
for where it could fall down.
workspace-sh/workspace— the consuming appworkspace-sh/table-file-format— the.tablefile format spec; see PR #26 for the permissions-side view