Resource guide
A mirror is a whole-drive replica of one workspace
A sync machine joins a workspace as a mirror: the entire workspace drive is replicated
and kept live in both directions. This is the transfer plane behind duet sync.
Mirror operations use your ordinary user login and current workspace membership.
Register a machine through POST /v1/user/sync-machines with its name and full
Syncthing device ID. The returned machineId is non-secret: two Macs can share
one login while retaining separate identities. Replacing a user key preserves them.
Register the calling device as a mirror
Registration exchanges device sync identities. Your device announces its own sync identity; the response returns the workspace VM's identity so your side can add it as the sole peer. The request names the registered machine independently of your login.
curl -X POST -H "authorization: Bearer $DUET_API_KEY" \
-H "content-type: application/json" \
-d '{"machineId":"machine_9","syncthingDeviceId":"K5MNBGJ-QT4EVLE-... (your device sync ID)"}' \
"$DUET_API_URL/v1/ws/acme/mirror-devices"{
"machineId": "machine_9",
"syncthingDeviceId": "K5MNBGJ-QT4EVLE-KPBQKY7-CPT4L2N-QU4WHNA-2XBTUXO-Q7XCLHS-6MZ4DAY"
}{
"machineId": "machine_9",
"folderId": "workspace-drive",
"vmSyncthingDeviceId": "R7ELVOD-P4WBW6H-... (the workspace VM's device sync ID)",
"tunnel": {
"url": "wss://acme-vm.exe.dev/v1/tunnel/accept/mirror-machine_9-a1b2c3",
"tunnelToken": "1767293700000.9f3ce12.4b8ac7",
"expiresAt": 1767293700000
}
}The full Syncthing device ID must match the registered machine. Edits arrive in
file activity as mirror:<machineId> and resolve to the machine's
user, including after removal. Registration is idempotent and returns a fresh
tunnel grant. An existing mirror can refresh at the workspace's admission limit.
What a mirror syncs
A mirror replicates the whole drive — one folder rooted at the workspace root, synchronized in both directions. Three things are always held back so machine-local state never crosses between machines:
.gitdirectories — a repository under two active writers corrupts; history belongs on a git remote, not in the mirror. Your working files still sync live; only the.gitbookkeeping is excluded.node_modules— platform-specific and reinstallable; syncing it clobbers one machine's binaries with another's architecture. Run your installer on each side..duet— Duet's own workspace state directory.
Everything else replicates, including .env files and other dotfiles. A mirror is a personal
replica of your own workspace, so its environment files travel with it — unlike a workspace
share, which withholds them by default. A mirror reaches every secret in the drive.
List the workspace's mirrors
curl -H "authorization: Bearer $DUET_API_KEY" \
"$DUET_API_URL/v1/ws/acme/mirror-devices"{
"items": [
{
"machineId": "machine_9",
"deviceName": "MacBook Pro",
"syncthingDeviceId": "K5MNBGJ-QT4EVLE-KPBQKY7-CPT4L2N-QU4WHNA-2XBTUXO-Q7XCLHS-6MZ4DAY",
"registeredAt": 1767290000000
}
],
"nextCursor": null
}Walk nextCursor until it is null; a short or empty page can still carry a continuation. The
returned devices let you confirm a registration landed or spot a machine you no longer recognize.
machineId is the non-secret identity used in file activity and machine settings.
Remove a device
curl -X DELETE -H "authorization: Bearer $DUET_API_KEY" \
"$DUET_API_URL/v1/ws/acme/mirror-devices/machine_9"{ "ok": true, "machineId": "machine_9" }The path names a machine you own. Removal detaches its folder membership and
invalidates future tunnel admissions for this workspace, leaving its other mirrors
and your login intact. Repeating removal succeeds. To remove a lost machine from
every workspace, use DELETE /v1/user/sync-machines/{machineId}.
Tunnel access, and re-minting it
A mirror connects over a tunnel, exactly as workspace-to-workspace shares do: your device dials the
url from the registration response and presents tunnelToken; the workspace VM bridges the
connection. Tunnel tokens are deliberately short-lived, and every reconnect must
present a fresh one — never cache a token across reconnects. Re-POST the registration route to mint
a new grant; a reconnect costs one request. An idle tunnel holds no workspace open: a
connected-but-quiet mirror never counts as workspace activity and never delays idle retirement.
Removal and membership changes
Removing a machine detaches its workspace mirrors and refuses later registration with that machine ID. Explicitly registering the same machine again restores its identity so you can turn sync back on. Leaving a workspace detaches your machines only from that workspace; the same user login continues to reach remaining memberships. Revoking the user key refuses subsequent requests and tunnel remints through the ordinary authentication gate. Existing tunnel lifetimes remain bounded.
Sync
3 operations /v1 /ws /{workspaceSlug} /mirror-devicesRegister the calling sync device as a whole-drive mirror of a workspace
- Scope
human- Request
Request JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "machineId": { "type": "string", "minLength": 1 }, "syncthingDeviceId": { "type": "string", "pattern": "^[A-Z2-7]{7}(-[A-Z2-7]{7})*$" } }, "required": [ "machineId", "syncthingDeviceId" ] }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "machineId": { "type": "string", "minLength": 1 }, "folderId": { "type": "string" }, "vmSyncthingDeviceId": { "type": "string" }, "tunnel": { "type": "object", "properties": { "url": { "type": "string" }, "tunnelToken": { "type": "string" }, "expiresAt": { "type": "number" } }, "required": [ "url", "tunnelToken", "expiresAt" ], "additionalProperties": false } }, "required": [ "machineId", "folderId", "vmSyncthingDeviceId", "tunnel" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Declared idempotent
/v1 /ws /{workspaceSlug} /mirror-devicesList the devices mirroring a workspace
- Scope
human- Request
- No JSON request body
- Query
Query parameters JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "cursor": { "type": "string", "minLength": 1 }, "limit": { "default": 50, "type": "integer", "minimum": 1, "maximum": 100 } }, "additionalProperties": false }- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "machineId": { "type": "string" }, "deviceName": { "type": "string" }, "syncthingDeviceId": { "type": "string" }, "registeredAt": { "type": "number" } }, "required": [ "machineId", "syncthingDeviceId", "registeredAt" ], "additionalProperties": false } }, "nextCursor": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "null" } ] } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }- Errors
Opaque collection cursor is malformed or bound to another walk (400)
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "error": { "type": "string", "const": "invalid_cursor" } }, "required": [ "error" ], "additionalProperties": false, "description": "Opaque collection cursor is malformed or bound to another walk (400)" }- Delivery
- Standard response
- Retry
- Not declared idempotent
/v1 /ws /{workspaceSlug} /mirror-devices /{machineId}Detach one device you own from a workspace mirror
- Scope
human- Request
- No JSON request body
- Response
Response JSON schema
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "ok": { "type": "boolean", "const": true }, "machineId": { "type": "string" } }, "required": [ "ok", "machineId" ], "additionalProperties": false }- Delivery
- Standard response
- Retry
- Declared idempotent