Core Concepts
Understanding these core concepts will help you get the most out of noBGP.
Networks
A network is an isolated overlay that connects your infrastructure together. Think of it as a private virtual network that spans across all your machines, clouds, and locations.
Key Features
- Isolation: Each network is completely separate - nodes in one network can't see or access nodes in another
- Secure: All communication within a network is encrypted end-to-end
- Distributed: Networks span across clouds, data centers, and edge locations seamlessly
- Authenticated: Access controlled by registration keys and OAuth permissions
Registration
Nodes join a network through registration. There are two ways to register:
- Interactive (recommended): Run
nobgp register— this opens your browser for OAuth sign-in, then lets you pick which network to join. No keys needed. - Key-based (for automation): Pass a registration key via
nobgp register --keyor theNOBGP_KEYenvironment variable. This is designed for Docker containers, CI/CD pipelines, and other headless environments.
Registration keys are base64-encoded tokens managed through the noBGP web dashboard. Treat them as secrets — don't commit to git.
After registration (either method), the agent receives a JWT token for ongoing authentication. The registration key is removed from the config file.
Default Network
Every noBGP account comes with a default network automatically. When you ask your AI assistant to provision nodes or show networks, it uses your default network unless you specify otherwise.
Nodes
A node is any machine connected to a noBGP network. Nodes can be:
- Cloud instances (AWS, GCP, Azure, etc.)
- Physical servers in your data center
- Raspberry Pis and edge devices
- Docker containers
Node Components
Every node runs the noBGP agent - a lightweight background process that:
- Maintains connection to the noBGP router
- Handles encrypted communication with other nodes
- Executes commands from your AI assistant (with your authorization)
- Reports system status and metadata
Node Identity
Each node has:
- Name: Human-friendly identifier (e.g., "web-server-1", "raspberry-pi")
- Network: Which network it belongs to
- Status: Online, offline, or connecting
- Metadata: OS, architecture, IP addresses, etc.
Node Discovery
Your AI assistant can discover nodes by asking noBGP:
Show me all nodes in my production network
This returns real-time status for all connected nodes.
Node Labels
Beyond what a node reports about itself (platform, OS, architecture), you can attach your own labels — key/value pairs that keep their type (string, number, boolean, or an RFC3339 timestamp):
Label the Raspberry Pis in production with role=edge and site=irvine
Labels do two jobs:
- They remember things across sessions.
last_checked: "2026-07-28T17:04:00Z"orreplicas: 3reads back unchanged next time. - They name a cohort. Anywhere noBGP asks "which nodes?" — watching files, dispatching a command, granting access — you can say
labels: {role: edge}instead of listing machines. Because cohorts are re-evaluated live, a node labelled later joins on its own.
Labels are set by you, never by the agent, so a node cannot tag itself into a cohort. Setting them is an organization Owner/Admin action: a label can move a node in or out of the reach a node_grant handed to another machine, so it sits at the grant's tier rather than the Member tier. See node_label.
DNS & Hostname Resolution
Nodes in the same network can reach each other by name. When the agent connects, it configures your system's DNS so that bare hostnames resolve automatically:
# From any node in the same network, reach others by name
ping web-server
curl http://api-server:8080/health
ssh raspberry-pi
Under the hood, each node gets its own overlay zone, <node-id>.nobgp.net, and the agent adds it to your system's resolver configuration as a DNS search domain. This means ping web-server expands to web-server.<node-id>.nobgp.net and resolves to the peer's overlay IP address — no manual /etc/hosts entries or DNS records needed.
The zone is answered by a DNS server built into the agent itself, running locally on the node. Lookups never leave the machine, so overlay names keep resolving even when the node cannot reach the router.
The agent integrates with your platform's native DNS to route the zone to that local server:
- Linux: resolvectl/systemd-resolved, NetworkManager, OpenWrt's dnsmasq, or
/etc/resolv.conf - macOS:
/etc/resolver/directory - Windows: NRPT rule plus the DNS suffix search list
On most hosts this is true split-DNS: only the overlay zone is routed to the
agent and every other name keeps its normal path. Some Linux configurations
offer no way to express that (plain /etc/resolv.conf, Alpine), so the agent
lists itself as the first nameserver and therefore sees every query. There
it forwards anything outside the overlay zone to the machine's own
nameservers, so ordinary internet names resolve exactly as before — including
for applications whose resolver treats a refusal as a fatal error rather than
trying the next nameserver (Node.js and Bun, among others). The host's
nameservers are re-read every minute, so a DHCP lease that swaps them is picked
up without a restart.
Hostname resolution works across clouds and locations. A node in AWS can reach a Raspberry Pi at home simply by name, as long as both are in the same network.
To look up a peer's overlay address directly — without going through your system's DNS — use nobgp resolve. It asks the agent for the address straight from the mesh directory, which is handy in scripts (ssh admin@$(nobgp resolve web-server)) or on hosts where overlay DNS is reported as unavailable.
Overlay Addressing
The overlay uses the 100.64.0.0/10 address space (the CGNAT range, which never collides with home/office LANs or the public internet). Each node self-assigns a small /20 slice of it for its own view of the network; peers appear as individual addresses inside that slice.
Two properties follow:
- Addresses are node-local. The same peer can have a different overlay address on each machine that talks to it. Always address peers by name — names resolve to the right address everywhere; raw overlay IPs are not portable between nodes.
- It coexists with other VPNs. If another interface already holds a 100.64.x.x address (Tailscale, WireGuard, a carrier connection), the agent picks its slice to avoid the conflict. This includes carrier-grade NAT uplinks like Starlink, where the ISP itself uses this range.
To pin the slice explicitly (e.g. for firewall rules), set overlay-cidr in the agent config or NOBGP_OVERLAY_CIDR — any /20 within 100.64.0.0/10:
overlay-cidr: "100.64.16.0/20"
nobgp show displays the slice in use; nobgp status lists each reachable peer with its current overlay address.
Addresses are stable. Once a peer has an address in your node's slice, it
keeps it — across agent restarts, and across the peer going offline and coming
back. If a peer leaves the directory, its address is reserved rather than
handed to someone else: the peer stops appearing in nobgp status (it is no
longer known to be reachable), but the address is held for it for 7 days. If
you look the name up while it is reserved, the agent checks with the router
first: a peer that still exists resolves to its usual address, a deleted peer
reports as not found right away, and if the router is unreachable the reserved
address is returned so a brief control-channel outage never breaks name
resolution.
Names that move between nodes. If a name a peer used to serve is republished on this node — you moved a service, or pointed a name at a different machine — the agent reconciles the two on the next directory update from the router:
- The old host is online and no longer advertises the name: its address is released right away, and lookups return the local one.
- The old host is offline: whether the name really moved is not yet
knowable, so its address stays reserved (and hidden from
nobgp status) while lookups already resolve to the local copy. The peer's return settles it — if it advertises the name again it keeps the address, otherwise the address is released. A reservation held this way still expires after the same 7-day window, so a decommissioned node cannot hold an address forever. - The old host still advertises the name too: nothing is released. The same name served from two nodes is a valid topology, and both keep their addresses.
Moving a name back later simply hands out an address again.
Services
Services are the way you expose functionality from your nodes to the internet. noBGP supports two types of services:
1. Proxy Services
Proxy services expose HTTP applications running on your nodes to the public internet with a unique URL.
Example use cases:
- Expose a web app running on
localhost:8080 - Share a development server temporarily
- Provide access to an internal dashboard
How it works:
Features:
- Automatic HTTPS with valid certificates
- Optional authentication (require OAuth to access)
- Enable/disable without deleting
2. Terminal Services
Terminal services provide browser-based terminal access to your nodes through a web interface.
Example use cases:
- Remote shell access without SSH
- Shared terminal sessions for collaboration
- Emergency access when SSH is unavailable
- Terminal access for air-gapped systems
How it works:
- Your AI assistant publishes a terminal service
- noBGP generates a unique URL (e.g.,
https://xyz789.nobgp.com) - Opening the URL shows a full-featured web terminal
- WebSocket connection provides real-time interaction
Features:
- Full terminal emulation (colors, control characters, etc.)
- Support for interactive programs (vim, nano, htop, etc.)
- Command history and tab completion
- Resize support
- Optional authentication
Service Authentication
Both service types support optional authentication:
- Auth required: Users must sign in with OAuth before accessing
- Public: Anyone with the URL can access (useful for demos, temporary shares)
Always use authentication for production services or those containing sensitive data.
Sharing Services with Specific People
When a service has authentication enabled, you control exactly who gets in. Visitors who aren't on your approved list see an "Access Required" page where they can request access with one click. You receive an email notification and can approve or deny from a built-in permissions dashboard — no configuration required.
You can also add people proactively by email before they visit, including whole domains with a wildcard (*@yourcompany.com).
See Access Control in the Publishing Services guide for the full details.
Sessions
A session is a running command on a node, managed by the AI assistant via the command MCP tool. Each session runs one specific command and closes automatically when that command exits.
Session Lifecycle
- Start: Your AI assistant calls the
commandtool with asessionobject specifying the node and command to run - Interact: The AI reads output using the returned
command_id; for interactive programs, it sends additional input - End: The session closes automatically when the command exits
Session Patterns
One-shot commands — run a non-interactive command and read its output:
df -h,systemctl status nginx,cat /var/log/app.log- Session closes as soon as the command finishes; no explicit cleanup needed
Interactive shell sessions — start bash (or another shell) and send multiple commands while maintaining state:
cdand exported environment variables persist across inputs within the session- The AI sends each command as stdin input to the running shell
- Close by sending
exitor aSIGTERMsignal
When to Use Each Pattern
Use one-shot commands for:
- Quick status checks (
uptime,df -h) - Non-interactive operations
- Scripted automation
Use interactive shell sessions for:
- Multi-step procedures that share state (navigate directories, set env vars, run related commands)
- Interactive programs like
vim,less,top,psql - Debugging with tools that require back-and-forth input
Subscriptions and Events
A session answers "what is happening on this machine, right now". A subscription answers "tell me when something happens", across as many machines as you like — without holding a connection open or polling.
There are three things you can subscribe to:
| Source | What it reports | Who does the work |
|---|---|---|
| Filesystem | Files and directories changing on the matching nodes | Each node runs a native watcher (inotify, FSEvents, ReadDirectoryChangesW) |
| Command | A command dispatched once to each matching node, and how each run went | Each node runs the command detached |
| Presence | Nodes coming online, going offline, or re-registering | The router — nodes run nothing |
Every subscription follows the same three steps: subscribe (you get a subscription_id), read events, unsubscribe. Reading is what keeps a subscription alive — it expires about 10 idle minutes after the last read — and unsubscribing stops the work on every node, not just the delivery.
Subscriptions are scoped by the same node selector everything else uses, so "watch /etc on every edge node" is one call rather than a loop. A node's owner keeps the last word over what that machine serves — which capability domains, which directories, whether elevation is allowed at all — and when a node won't serve a source it says so with a refused event instead of going quiet.
See the Monitoring & Events guide for the full workflow.
Authentication & Security
noBGP uses multiple layers of security:
OAuth 2.0
All AI assistant access is protected by OAuth:
- Sign in with Google, GitHub, or other providers
- Token-based access control
- Automatic token refresh
- Per-session permissions
Agent Registration
Nodes authenticate through a two-phase process:
- Registration: Either via OAuth browser login (
nobgp register) or with a registration key (nobgp register --key) for automation - Ongoing auth: After enrollment, the agent receives a JWT token signed with per-network Ed25519 keys — no key or login needed for subsequent connections
- Registration keys are unique per network and can be rotated without downtime
Encryption
All communication is encrypted:
- TLS for web traffic
- End-to-end encryption within the overlay
- Certificate pinning for agent connections
Authorization
Not all users can do everything:
- Node provisioning requires account approval
- Network management requires an Owner or Admin role in the owning organization
- Command execution requires organization membership
File Sharing
noBGP provides a shared virtual drive scoped to each network. This allows you to share files across all nodes and manage them through a web interface.
How It Works
Virtual Mount Point:
- Each network gets a shared drive mounted on every connected node
- All mounts are backed by S3 storage
- Files written on one node are visible on all other nodes in the same network
- Default mount points:
/mnt/nobgp(Linux),/Volumes/nobgp(macOS), drive letter (Windows)
Mount Technology: see Which backend mounts the drive below. Whichever backend the node lands on, the agent runs a small proxy on 127.0.0.1:19840 that adds the node's credentials to the requests it forwards, so the operating system's own client needs no noBGP configuration of its own. On Linux and macOS you can restrict which local accounts may reach it — see Shared drive proxy keys.
Multi-Profile Mount Points:
When running multiple profiles (see Multi-Profile Support), each profile gets its own mount point with a -{profile} suffix (e.g., /mnt/nobgp-work, /mnt/nobgp-staging). The default profile always keeps the bare name (/mnt/nobgp). On Windows, each profile gets a separate drive letter.
File Names:
- Names are stored exactly as written, and are unique without regard to case (router 0.4.71).
Foo.txtkeeps its capitals forever — nothing is lowercased, folded or rewritten, and a listing gives back the bytes that were written — butfoo.txtis the same name, so creating it besideFoo.txtis refused asalready_exists, naming the spelling that holds the name. Over the share's URL that refusal is a 409. Overwriting the stored spelling is ordinary and always allowed, and renaming a name to another spelling of itself (foo.txt→Foo.txt) is how a stored casing gets corrected. - Lookups are still exact.
README.mddoes not findreadme.mdthrough the file tools or the share URL; only the mount folds, on the platforms whose own filesystems do. The rule exists so a share can be copied onto a Windows or macOS filesystem at all — neither can hold two names differing only in case, so a directory that did lost a file silently on the way off the share. It is not retroactive: a directory that already holds both spellings keeps both, both readable and both writable, and only a new second spelling is refused. Between router 0.4.53 and 0.4.71 either spelling was a separate file; before 0.4.53 both reached the same one. See the changelog. %, spaces,#and?are ordinary characters in a name and survive the round trip through the mount, the share URLs and the file tools unchanged. On the mount this needs agent 0.4.56: before it, a file created on the drive asa%20b.txtwas stored asa b.txt, and a#or?cut the path short so the write landed somewhere else entirely..nobgp-upload-…is reserved. The share writes an upload under that prefix and renames it into place when the body is complete, so a name of your own beginning with it is refused rather than stored.- OS metadata files are accepted and discarded —
.DS_Store,._*,desktop.ini,Thumbs.dbandautorun.infnever take up space and never appear in a listing, so copying a folder from Finder or Explorer does not litter the share.
Web Dashboard:
- Browse a network's files in the web dashboard: open Networks, choose your network, then Files
- Signed in with your noBGP account — the same access your account already has to that network
- Upload and download files through your browser
- Manage directories and organize your shared files
What the mount contains
One mount, two folders (agent 0.4.54+):
/mnt/nobgp/
├── node/ this machine's own storage area
└── networks/
└── production/ one folder per network this node belongs to
-
node/is the node's own storage area — the same tree ashttps://files.nobgp.com/nodes/<node-id>/, reachable from the machine itself with no URL and no token. It sits at that exact path on every node, never named after the machine and never carrying its id, so the same script deployed across a fleet reaches each machine's own storage by the same path. -
networks/<name>/is a network's shared drive, one folder per network the node belongs to, carrying the network's name. Everything above about the shared drive applies inside it. -
The root and
networks/are a view, not storage. Creating, deleting or renaming directly in either is refused as a read-only directory — a file written there would have nowhere to live. Everything belownode/andnetworks/<name>/is read-write exactly as before. -
A file cannot be moved between the two trees in one step.
node/and each network are separate filesystems, so a move across them cannot be a rename. On Linux the mount says so in the waymv, Finder and Explorer understand, and they copy and then delete for you. On macOS and Windows the move is refused (agent 0.4.56+) — copy the file across and delete the original. -
The layout is the same whichever backend mounts the drive. On macOS and Windows, run agent 0.4.56 or later: 0.4.54 presented the two folders correctly but dropped entries from the listings inside them, so a folder holding six files could show four with nothing reporting an error, and 0.4.54–0.4.55 turned a rename in Finder or Explorer into a wrongly-placed new file inside the share. Linux was unaffected by both.
-
Renaming a network reaches the mount right away, from agent 0.4.75. noBGP pushes the node's whole network list down the connection it already has, so
networks/<old>/becomesnetworks/<new>/on every node in the network at once — no reconnect, no restart. What can still lag is the operating system's own directory cache, bounded byfs-cache-ttl, which is 30 seconds unless you have changed it.Through agent 0.4.74 the mount learned its network names only when the agent connected, so the old folder name stayed until that node next reconnected — up to a day, and by a different amount on every node depending on when each last connected. Nothing was lost or misfiled in the meantime, since the folder still pointed at the same network, but two machines could show different names for it, and every rename also put a spurious
received unknown messagewarning in each node's log.
Before 0.4.54 the mount root was the network's shared drive, so a file that used to be at /mnt/nobgp/report.pdf is now at /mnt/nobgp/networks/production/report.pdf. Nothing moved in storage; only what the mount shows you changed. The networks/ level exists so that a network named node cannot shadow the one path that has to mean the same thing on every machine.
On macOS, the agent cannot read its own mount
The drive a Mac mounts works normally for everything on that machine — Finder, a login shell, sudo from Terminal, and any app you open. The one process that cannot read it is the agent's own background daemon, which means the file tools refuse a path under the mount point (/Volumes/nobgp by default) with a bare Operation not permitted.
This is macOS's consent layer (TCC) gating access to a network volume on the process doing the reading. A background daemon has no consent and cannot ask for one, so no retry, no execution identity and no backend change makes a difference — measured across eleven variations on 2026-08-12, including mounting as the console user, mounting outside /Volumes, and webdav in place of nfs. Linux and Windows nodes have no equivalent restriction.
What it costs, and what it does not:
- Nothing about the drive itself. Reading, writing, locking and syncing all work for the machine's own users and programs. The mount is healthy; the daemon is blind to it.
- The file tools reach that content a different way. Ask the router for it instead of the node —
network_namealone for a network's shared drive, orstorage: truefor a node's own area. Those serve the same bytes, and the node need not even be online. See Addressing storage instead of a node. - The share's HTTPS endpoint is unaffected, from a Mac or anywhere else.
- Granting the
nobgpdaemon Full Disk Access does lift it, for every tool at once. On that Mac: System Settings → Privacy & Security → Full Disk Access →+, then press ⌘⇧G (the picker hides/usr/local) and enter/usr/local/bin/nobgp— the daemon's own binary. It takes effect on the running daemon immediately, with no restart and no remount. It takes a person at that machine, or an MDM profile: nothing sent over noBGP can grant macOS consent, which is why the file tools address the remedy to the node's owner from router 0.4.76. Since agent 0.4.72 the grant survives upgrades. The macOS binaries are signed with a Developer ID certificate, so macOS keys the consent to the signing identity rather than to the exact bytes, and a later build satisfies it. Under 0.4.71 and earlier the binaries carried an ad-hoc signature, which tied the grant to that one binary and let the next auto-upgrade void it silently — so on those versions the grant was worth doing by hand on a machine you were debugging and not worth rolling out. A node upgrading from such a version needs the grant given once more, against the signed binary; from then on it holds.
Which backend mounts the drive
The drive is presented to the operating system by one of four backends, and the agent mounts with the first one this host can actually serve:
| Backend | What it is | Available when |
|---|---|---|
nfs | The agent's own NFSv4 server on loopback, mounted by the kernel's own NFS client. Runs as an ordinary user on a high port — no rpcbind, no privileged port, nothing to install on macOS. From agent 0.4.69 its locks hold between nodes too | The kernel can mount NFSv4 — and on Linux, from agent 0.4.70, that is the whole question: with no mount helper installed the agent makes the mount itself, so a host with no NFS packages at all can serve this backend (see below). On macOS the helper is mount_nfs, part of the base system. Agents 0.4.62–0.4.69 required a helper on Linux too, which is nfs-common/nfs-utils; from agent 0.4.58 install.sh installs it on Debian/Ubuntu, RPM hosts and Arch |
fuse | An in-process filesystem over /dev/fuse, with on-disk caching — files are cached locally for fast reads and uploaded asynchronously on writes. From agent 0.4.64 its locks hold between nodes — the first backend where they did, joined by nfs in 0.4.69 | /dev/fuse is usable. Linux only today. The userspace fusermount helper (fuse3, or fuse-utils on OpenWrt) is needed as well, but that only shows up when the mount is attempted — see Shared drive is empty and never mounts |
winfsp | An in-process filesystem over the WinFsp driver — the same idea as fuse, with a Windows driver underneath, and from agent 0.4.67 with the same on-disk caching and asynchronous write-back. Read-write from agent 0.4.63 (read-only in 0.4.62), though a real mount only accepts writes from 0.4.65 — and opt-in throughout: auto never chooses it. Agent 0.4.62+ | WinFsp is installed and its library loads for this machine's architecture — and from agent 0.4.73 the probe checks the version too, so an install older than 1.10 (2022) reports as unavailable with an upgrade remedy instead of being chosen and failing the mount. Windows only |
webdav | The operating system's own WebDAV client against the agent's local proxy — mount_webdav on macOS, net use on Windows, davfs2 on Linux | The proxy is running and the OS mount helper exists (nothing to check on Windows) |
Whether a backend can serve is a question about the host, and from agent 0.4.57 it is always answered by probing that host rather than assuming from the platform — is there an NFSv4-capable mount helper and a kernel that can mount NFSv4, is /dev/fuse usable and its helper on the path, does the WinFsp library load. Which of the capable backends wins when you have not pinned one is a different question, and from agent 0.4.65 it has a different answer per platform:
| Platform | Preference order |
|---|---|
| Linux | fuse → nfs → webdav |
| macOS | nfs → webdav (agent 0.4.65 alone had these the other way round — see below) |
| Windows | winfsp → webdav |
| Synology DSM | fuse alone — DSM's only package manager has neither davfs2 nor an NFSv4 client, so there is no second backend to fall to |
| OpenWrt | as Linux, but kmod-fuse + fuse-utils come from the installer and NFSv4 and davfs2 only if you install them by hand |
| Containers | as Linux: privilege to mount at all, then /dev/fuse for fuse and nothing for nfs |
webdav is the fallback on every platform and is never removed from a chain — but it is probed like the others, so a Linux box without davfs2 reports it as unavailable rather than pretending it could mount. On Linux that is the common case rather than the exception, and worth knowing before you plan around it: install.sh tries fuse3 first and only falls back to davfs2 if that fails, which on a Debian-family host it almost never does, and on OpenWrt, Arch and Synology DSM it does not try davfs2 at all. So a Linux node's chain is usually two backends deep, not three, and on Synology — whose only package manager has neither davfs2 nor an NFSv4 client — it is one. If a host can serve none of them, nothing is mounted and fs.error says no filesystem backend is available on this host; installing davfs2 by hand adds the fallback where the platform has it. macOS has mount_webdav in the base system, and Windows needs nothing.
A backend that cannot serve a platform at all is not in that platform's list, and nobgp status no longer names it: there is no FUSE build for macOS or Windows, no WinFsp anywhere but Windows, and no NFSv4 client on Windows (it ships v2/v3 only, which cannot mount the agent's v4 server). The line it used to print was misleading anyway — telling a Mac owner that /dev/fuse is not usable invites installing something that will not help. The name still parses everywhere, so fs: fuse in a config carried to a Mac is refused by name rather than read as a typo.
Falling through happens on availability, never on failure. If the chosen backend is available and its mount then fails, the agent retries that same backend rather than dropping to the next one — cascading down the list on a failure is how a node ends up flapping between two broken mounts. Once mounted, the agent probes the mount every 30 seconds and remounts after three consecutive failures, about a minute. A healthy mount is left alone, so installing a backend the node would have preferred changes nothing until the next nobgp service restart.
Limitations worth knowing before you rely on one
These are properties of the platform, not bugs waiting on a release:
- ⚠ On macOS, the agent's own service cannot read the node's mount. File operations and commands the agent runs for you against
/Volumes/nobgpfail withOperation not permitted, at every identity including root. This is macOS TCC consent, not a permission or privilege problem. Your own account reads the mount normally; only the agent's service is blind. See On macOS, the agent cannot read its own mount. - ⚠ On macOS the two backends trade against each other, and the default takes the honest lock. Both serve — an earlier result where WebDAV did not finish a 60-file read in 328 seconds did not reproduce when both were re-measured on one Mac on agent 0.4.77. NFS answers questions about files and folders locally where macOS's WebDAV client goes back over the network for each one, and WebDAV is quicker at creating, renaming and deleting them.
nfsstays first because its locks are real: on a macOS WebDAV mount two processes can each take an exclusiveflockon one file and both be told they hold it. See what each backend measures at on a Mac. - ⚠ WebDAV is not available on much of the Linux fleet. It needs
davfs2, which the installer only reaches for when FUSE cannot be installed — which almost never happens on Debian, Alpine or Amazon Linux — while Arch, OpenWrt and Synology have no automatic fallback to it at all. Synology cannot supply it at any price:synopkg, DSM's only package manager, has no WebDAV package, and DSM registers no NFSv4 client either, so a Synology node has exactly one backend and nothing behind it. - ⚠ WinFsp on Windows is opt-in and needs WinFsp 1.10 or newer.
autostill selects WebDAV on Windows. WinFsp is otherwise the better drive there — machine-wide rather than confined to one logon session, and not labelledDavWWWroot— but two things hold it back, andnobgp statusnames both: seewinfspon Windows. - On WebDAV, nothing guards against overwriting another node's change. The other backends condition every upload on the version they read and refuse when it has moved; the operating system's own WebDAV client does not, so two nodes writing one file is last-writer-wins and neither is told. See The mounted drive uses it too.
- On FUSE, a write can fail after your program was told it succeeded. Uploads are queued, so
write,fsyncandcloseall return success and the upload can be refused seconds later. The bytes are not lost — the rejected copy is kept beside the cached file under a.rejectedname and the agent logs where — but nothing tells the program that wrote it, at the time. NFS reports a refused write properly, atwrite,fsyncorclose. - Reads and writes move whole files, on every backend. Reading 4 KB out of a 1 GB object fetches 1 GB; changing 4 KB inside a 10 MB file uploads 10 MB.
What changed in 0.4.65 and 0.4.66
Through 0.4.64 one fleet-wide order put nfs first on every platform. Nothing about the drive's contents or layout changes when the backend changes, and pinning fs holds any node exactly where it is.
-
Most Linux nodes move from
nfsback tofuse(agent 0.4.65). FUSE caches file contents and directory entries locally, so re-reading a tree it has already seen costs nothing, and it was then the only backend whose locks hold between nodes —nfscaught up in 0.4.69, so that half of the reason has since expired.nfskeeps second place as the fallback for a Linux box with no usable/dev/fuseor nofusermounthelper, which is a real population — such a host is unaffected and stays on NFS. Pinfs: nfsto keep NFS on a node that has both. -
Macs moved to
webdavin 0.4.65 and are back onnfsin 0.4.66. The move was a safety measure, not a performance one: two macOS kernel panics had been recorded inside the system's own NFS client, and being wrong about the cause costs the whole machine rather than one operation. What the fleet showed within the release was the price — the Macs became the only nodes reporting that a lock on their drive is not honestly enforced (see what each backend does with a lock below), because macOS WebDAV tells two processes they both hold the same exclusiveflockwhile NFS refuses out loud, and a WebDAV write-back is never conditional either. The panics have since been attributed to the agent's own NFS test suite rather than to a mount — the long-lived mounts on those machines have never panicked one — so 0.4.66 putsnfsback in front, and it lands together with the shared cache that makes an NFS mount cheap to re-read.The reason is the locking, and it holds whatever the speeds turn out to be — which is worth saying plainly, because this page previously said WebDAV on a Mac did not serve at all and that turned out to be wrong. See the numbers below.
-
Windows is unchanged in practice.
winfspleads the list, butautostill declines it (see below), so a Windows node keeps mounting over WebDAV exactly as it did.
Which NFS client a Linux node has is largely a matter of when it was installed. From agent 0.4.58 the install script installs an NFSv4 client on Debian/Ubuntu, RPM hosts and Arch — worth having as the fallback a host without FUSE lands on. sudo nobgp upgrade does not install it — no package declares the client as a dependency, deliberately, because the installer is also what confines the rpcbind it pulls in — so a node upgraded in place keeps the backends it could already serve. Alpine, OpenWrt and Synology are left out of that on purpose; from agent 0.4.70 a Linux host needs no package at all to mount over NFS, only a kernel that has the v4 client, so those three can now take this backend as well.
The two macOS backends, measured
Both macOS backends were run against the same 60-file fixture on two Macs on agent 0.4.77 — one on Ethernet and idle, one on Wi-Fi and busy. This replaces an earlier reading on this page that WebDAV on a Mac "did not serve": it completed every phase on both machines.
| NFS (Ethernet, idle) | WebDAV (Ethernet, idle) | NFS (Wi-Fi, busy) | WebDAV (Wi-Fi, busy) | |
|---|---|---|---|---|
| Walk a folder it has never seen | 0.5 s | 3.9 s | 0.5 s | 7.5 s |
| Ask about 60 files it has seen | 0.02 s | 3.8 s | 0.01 s | 7.2 s |
| Write 8 MB | 0.8 s | 0.6 s | 14.4 s | 6.6 s |
| Create 60 files | 40 s | 15 s | 250 s | 57 s |
| Rename 60 files | 43 s | 12 s | 168 s | 48 s |
| Delete 60 files | 32 s | 9 s | 50 s | 18 s |
Read the ratio, not the seconds. The same trade holds on both machines — NFS is far quicker at looking at a folder, WebDAV 2.6–4.4× quicker at changing one — but the absolute cost is dominated by the machine and its network rather than by the backend: the same touch of the same file on the same build cost 0.4 s on one of these Macs and 3.4 s on the other. If creating files on the drive feels slow from a Mac, look at the link before you look at the backend.
What that means in practice:
- Work that reads and searches — a build, a backup,
git statusover the drive — is where NFS answers locally and WebDAV asks the network for every question. - Work that creates and renames a lot of files pays on either backend, and most on NFS. Part of that cost is a defect rather than the protocol: macOS writes a hidden
._namecompanion beside every file it creates on a network volume, the share deliberately throws those away, and the mount can end up retrying its own write because of it. A fix is in progress; the rest of the gap is NFS confirming each change is stored before it says the change happened, and will remain. - The lock is why the default does not follow the create numbers. WebDAV's price for being quicker at changes is that it cannot lock: two nodes writing one file is last-writer-wins, and on macOS an exclusive
flockreturns success to both holders. See what each backend does with a lock. fs: webdavis still not a proven escape hatch on a Mac. What was measured is the WebDAV mount, made by hand beside a live NFS one. The agent falling back to it on its own — the probe, the order, and the switch a failing NFS mount would take — has not been exercised on macOS. If NFS cannot mount on a Mac,fs: offis the answer that is known to behave.
What nfs needs on a host
An NFS client is two separate pieces: the userspace mount helper (/sbin/mount.nfs4 on Linux, mount_nfs on macOS) and the kernel's own NFSv4 client. On Linux, from agent 0.4.70, only the second one is required. With no helper on the box the agent performs the mount itself, with the same options the helper would have passed — there is nothing for a helper to resolve on a loopback mount — so a container with no NFS packages installed mounts fine. That is worth having on Alpine above all, where the client package costs 34 packages and 57 MiB and pulls in rpcbind and python3, neither of which a single-port NFSv4 loopback mount uses. A helper that is present is still used in preference, so nothing changes on a node that already had one. On macOS the helper is part of the base system, and its absence there means a broken install rather than a missing package.
Through agent 0.4.61 only the helper was probed, so a box that had the helper and a kernel without NFSv4 reported nfs: available, was selected best-first, failed the mount — and, because a failed mount deliberately never falls through to another backend, was left with no shared drive at all. Embedded distributions are where the two come apart: on OpenWrt nfs-utils is the helper alone and kmod-fs-nfs-v4 is the kernel half.
From agent 0.4.62 the probe asks the kernel too, and nobgp status names what is missing, because the two halves have different remedies:
backends:
- "nfs: unavailable (this kernel has no NFSv4 client (no `nfs4` in /proc/filesystems after a module load; OpenWrt: `opkg install kmod-fs-nfs-v4` matching the running kernel))"
A kernel that has the modules on disk but has not loaded them still counts as capable: the agent asks the kernel to load them before deciding, and retries that every five minutes, so a node where you install kmod-fs-nfs-v4 picks NFS up on a later mount cycle without waiting for a restart. Inside a container there are no modules to load and no privilege to load them, yet the host kernel loads its own filesystem modules on demand — so from agent 0.4.70 the probe also settles the question by attempting a throwaway mount against a closed loopback port, which fails in microseconds on a capable kernel and registers the client on the way. The verdict is the kernel's registered filesystem list either way. On macOS nothing changes — the client is in the kernel and mount_nfs is part of the base system, so the helper is the whole question there.
winfsp on Windows
Windows is the one platform still mounting over WebDAV by default, and two long-standing quirks of the Windows WebDAV redirector come with it: the drive letter is visible in exactly one logon session, and Explorer labels the volume DavWWWroot. A WinFsp volume has neither — it registers a single machine-wide drive letter through the Mount Manager, and from agent 0.4.76 Explorer shows it as noBGP.
Through agent 0.4.75 it carried no label at all — vol N: answered "Volume in drive N has no label" — and Explorer fills that gap with its generic name for an unnamed local volume, so the drive read as Local Disk, indistinguishable from a real disk on a machine with several letters in use. The label is deliberately not per profile: the mount point already carries the profile name, and on Windows that means two profiles land on two drive letters, which is what a path uses and what tells them apart.
It was read-only in agent 0.4.62 — every write refused, so writing to the share from that node had to go through the file tools or the share's URL instead. From agent 0.4.63 it writes: create, write, truncate, rename, delete and mkdir/rmdir all work, and every write-back carries the same version check the nfs and fuse mounts use.
Writing on a real mount needs agent 0.4.65, though. On 0.4.63 and 0.4.64 the volume mounted, listed and read perfectly while every attempt to create a file was refused with Access is denied — from every account on the machine, including the one the agent itself runs as. Windows decides that before any of the agent's code is reached, from a security descriptor WinFsp builds out of what the filesystem reports about ownership, and what the volume reported granted full access to an account identifier no token on the machine holds, leaving read-and-execute as the only thing any real caller matched. From 0.4.65 the mount is given an explicit descriptor — full access for LocalSystem, Administrators and Authenticated Users, which covers the agent, the desktop account and the node's configured user — so creates, writes and renames work. The same release stops the volume reporting zero free space, which Explorer refuses a copy into before it starts. This needs WinFsp 1.10 (2022) or newer, because an older WinFsp rejects the option that carries the descriptor and refuses the whole mount rather than the one option it does not know.
From agent 0.4.73 the version is part of the probe, so an older WinFsp is reported as unavailable and never selected, rather than being pinned or chosen and then failing every mount cycle. It is a third state with its own remedy — "not installed" says install it, this one says upgrade it — and it names what it found and where:
backends:
- "winfsp: unavailable (WinFsp 1.7 is installed at C:\Program Files (x86)\WinFsp\ but this backend needs 1.10 or newer (older WinFsp refuses the whole mount rather than the one option it does not know) — upgrade it: install WinFsp (`winget install WinFsp.WinFsp`, or https://winfsp.dev))"
A DLL whose version cannot be read is treated the same way, deliberately: the node stays on the backend it already had rather than guessing.
It stays opt-in all the same. Through agent 0.4.72 the reason given was that the one real mount it had run on lost data; that was fixed in 0.4.69 and confirmed on hardware on 2026-08-11, and from agent 0.4.73 the reason says what is true now instead. Two things are left, in the order they have to be fixed:
- Locks are node-local. This backend has no way to forward a lock to noBGP, so an exclusive open holds between processes on that machine and not against a writer on another node — see what each backend does with a lock. A losing cross-node write is still refused rather than blended, because every write-back is conditional on the version the handle read.
- Creating a file is slow — about a second each when it was last measured on real hardware, which anything unpacking an archive onto the volume meets immediately. Agent 0.4.73 removes one of the three round trips a create costs (see below); the rest has not been re-measured, so the reason stays and the figure does not.
A third reason, case-sensitivity, was answered in agent 0.4.74 (see below) and came off the printed reason in agent 0.4.75. Agent 0.4.74 alone printed all three: on that one release, read the case clause as already answered and the two above as current.
Auto-selecting would hand every Windows box that happens to have WinFsp installed — and WinFsp is a shared driver that rclone, sshfs-win, Cygwin and MSYS2 all install — a filesystem in place of the WebDAV drive it has today, a working drive replaced after an upgrade with nothing but a backend name to explain it. nobgp status says both halves rather than reporting it as plain "available":
backends:
- "winfsp: available, but not chosen automatically — its locks are node-local rather than forwarded to the router, and a file create is slow — about a second when it was last measured, since improved by one round trip and not yet re-measured (set `fs: winfsp` to use it)"
To try it, install WinFsp (winget install WinFsp.WinFsp, or winfsp.dev), pin fs: winfsp in the profile, and restart the agent. Leave fs alone and a Windows node keeps its WebDAV mount exactly as before.
From agent 0.4.67 a winfsp volume reads and writes through the node's own disk cache, the one the fuse and nfs backends already use. Until then every open downloaded the file again into a temporary of its own, which made it the most expensive backend per operation and gave two handles on one path two private copies — a write through one was invisible to the other. Now there is one cached copy per path: opening a file the node has already downloaded and nothing has changed costs no request at all, both handles read and write the same bytes, and the copy outlives the handle rather than being deleted at its last close, so the next open is free too. It is bounded like the others — an hour since last use, 1 GiB, 100 MiB of free disk kept back — and the versions it remembers do not survive an agent restart.
The same release makes the upload asynchronous, the shape a fuse mount has always had. Through 0.4.66 the write-back happened inside the CloseHandle that triggered it, so a refused save failed that call; now the bytes are queued and carried up behind the caller, and a refusal arrives after the close has already returned success — see the mounted drive's version check for where it surfaces instead. fsync jumps the queue's coalescing delay, so a program that wants its save sent now should call it. Deleting or renaming a file whose upload is still queued takes the queued bytes with it, so a deleted file is not recreated seconds later; the one gap is a file inside a renamed directory that had no handle open on it and an upload still pending, which goes up under the directory's old name.
Two spellings of one name, from agent 0.4.74
From agent 0.4.74 a winfsp volume is case-insensitive and case-preserving — what every Windows program assumes, and what the WebDAV drive this backend would replace already gave them.
- A mis-cased path resolves.
Get-Item CASE.TXTfindscase.txt, and so do open, read, write, truncate,mkdir, delete and rename: every operation that takes a path resolves it for itself, so the file's contents are found under the folded name as well as its size and timestamps. Through 0.4.73 a wrong-case read failed outright. - The stored spelling is what comes back. Listings, Explorer's address bar and a shell's tab completion show the name as the share stores it, not the case you typed.
- An exact spelling costs nothing. A path that came out of a listing — which is nearly all of them — matches byte for byte and never folds. Folding is a fallback, applied one path component at a time against listings the node already holds, so a path under a directory it has seen recently costs no extra requests.
- It is safe because the share refuses collisions. Router 0.4.71 made names on the storage trees unique without regard to case, so a fold can never have two files to choose between. A directory that already holds
Foo.txtandfoo.txt— from before that rule, or written straight to the bucket behind noBGP's back — keeps both, both listed and each reachable by its own exact spelling. Only a third spelling that is neither of theirs is folded, and it resolves to the same one of them on every lookup rather than to whichever came up first. - Only this backend folds. A Linux
fuseornfsmount still presents the share exactly as it is stored, because that is what those platforms' own filesystems do and what every script on them assumes. The rule is Unicode simple case folding, the same one the share enforces:K(U+212A) is the same name ask,İ(U+0130) is not the same name asi, and composed and decomposedéstay two names. - A router it cannot reach costs the fold, not the operation. Resolving is best effort and stops at the first component it cannot answer, leaving the rest of the path as you wrote it — so a blip degrades to the pre-0.4.74 behaviour instead of failing the call.
Creating a file, from agent 0.4.73
Every create on this volume used to ask noBGP whether the path existed before claiming it, and that question could never be answered from the node's cache: a directory listing tells you what is there, never that a name is absent, so the check was a request guaranteed to come back "not found" in front of every single create. It is gone. The existence question is asked of the directory listing the node already holds, and the claim itself is what settles it — a create takes the path exclusively, so being wrong about the answer costs a refusal rather than someone else's file.
Two behaviours change with it, both on a path this mount has no warm listing for:
- A create onto a file that already exists answers
EEXISTwhere it used to empty the file and carry on. For a caller that asked to create a file that is the right answer, and Windows applies the caller's own disposition to it — an overwrite-minded program retries as an open-and-resize and gets what it wanted. - A create onto an existing directory answers
EISDIRrather than queueing a whole-file write over it.
Appending to a file the volume just wrote
Agent 0.4.69 fixes a data loss on a winfsp volume, found on the one real mount this backend has run on (2026-08-10). Windows asks a file for its size by name as well as through an open handle — that is what every dir, Get-Item and Explorer size column does — and the by-name question fell through to the cached listing, which still held the size before the write. Windows then computes an append's write offset from exactly that number, so Add-Content on a file this volume had just created landed at offset 0 and destroyed what was there, while reporting success and uploading the bytes perfectly.
There were two doors to it, and 0.4.69 closes both:
- While a handle is open, a by-name question is now answered from that handle's unsaved bytes, the same answer the handle itself already gave.
- After the last handle closes, it is answered from the pending write-back for as long as the upload is queued or in flight — nominally about five seconds, and up to five minutes when the upload is retrying under backoff. The answer has to outlive the handle because the upload does:
Set-Content f; Add-Content fcloses the file between the two commands and lands in exactly that window.
The fix was confirmed on that same hardware on 2026-08-11 — the size correct immediately after a write, Add-Content landing at the right offset, the share's copy matching byte for byte — so from agent 0.4.73 this is no longer among the reasons auto declines the backend. Agents 0.4.69 through 0.4.72 went on reporting it in nobgp status after it had been fixed. On agent 0.4.67 and 0.4.68 — the releases where the volume both wrote and deferred its uploads — do not append to a file on a winfsp mount; write it whole, or use the file tools or the share's URL. webdav, the backend every Windows node runs unless you pinned otherwise, was never affected.
How much a Linux nfs mount moves per request
Through agent 0.4.70 a Linux nfs mount moved bytes 1 KiB at a time. The mount has always asked for 128 KiB reads and writes, and macOS gives it exactly that — but the Linux kernel client sizes its transfers from what the server says it can serve, and the server said nothing. Linux reads a missing maximum as an unknown one and falls back to its own 1 KiB floor, whatever the mount asked for, with nothing logged and no error anywhere: the mount succeeded, the drive listed and read correctly, and every byte crossed in 1 KiB pieces. An 8 MiB write became roughly 8192 separate write requests, measured at 8.8 seconds against 0.1 s for the same bytes on a fuse mount on the same machine.
From agent 0.4.71 the server advertises the 128 KiB it will really serve, so the client negotiates 128 KiB and that same write costs 64 requests. Nothing about mounting changes — same port, same options, same drive — and there is nothing to configure: upgrading the agent and letting the drive remount is the whole of it. What the mount settled on lives in the kernel's own view of it rather than in the mount options, so read it there:
nfsstat -m # or:
grep nobgp /proc/mounts # rsize=131072,wsize=131072 from 0.4.71
macOS was never affected — it honoured the requested size with or without the advertisement — and no other backend was. Which Linux nodes carried it is worth knowing, because auto prefers fuse there: it bound the hosts with no usable /dev/fuse and the nodes pinned to fs: nfs.
The nfs server was replaced in 0.4.69
From agent 0.4.69 the NFSv4.0 server behind the nfs backend is a different implementation. Nothing about mounting it changes — same loopback port, same mount options, same client, same drive — but the old library had no NFSv4 state machine at all, which is what put a ceiling on several behaviours at once:
- Locks are forwarded to noBGP, the headline of the change and the reason for it. An
nfsmount joinsfuseas a backend whereflockandfcntlmean something on a second node. fsyncreports a refused write-back. The old server discarded the error from bothfsyncandclose, so a save the router refused could only be discovered from the nextwriteon the same handle. A program that callsfsyncand checks its return value now learns straight away.rmdiron a directory that still has something in it is refused, with "directory not empty". Before this it deleted the whole subtree and reported success — the share's own delete is recursive, and the emptiness check the old server made could never fire against a remote tree.rm -ris unaffected either way: it empties the directory through the mount before it callsrmdir.- Two writes from different nodes inside the same second are told apart. The marker the kernel uses to decide a file has changed was derived from the modification time, which is second-granularity, so a peer's write landing in the same second as the one before it could be missed. It now comes from the share's own version identifier.
- Bytes a client had buffered but not committed survive an agent restart. The client re-sends them when it notices the server restarted, where before they died with the agent.
truncateon a file nobody has open takes effect immediately, rather than waiting for a close that is never coming.
Under nfs, a file is fetched whole the first time something actually reads or writes its contents, and uploaded whole when it is closed, and only if it was modified — the share has no partial write, so a small edit to a large file is a full upload however it is made. Directory and attribute lookups are cached by the kernel, which is what keeps an ls -l of a deep tree from becoming a request per entry — for a fixed minute through agent 0.4.66, and from 0.4.67 for as long as you tell it to, the same window the agent's own listing cache behind it uses.
Re-reading a file the kernel has already cached costs nothing from agent 0.4.65. Through 0.4.64 the bytes were fetched when the file was opened, so a second pass over a tree spent the same whole-file downloads as the first even though the kernel's own cache meant it never asked for a single byte — on a 60-file tree, opening every file without reading any of them took longer than reading the whole tree cold. The fetch now happens on the first operation that needs the contents, so a warm read touches noBGP zero times. A file another node has changed is still re-read: the kernel notices the change on its next open and asks for the bytes, which is what triggers the fetch.
And from agent 0.4.66 the fetch itself is served from the node's own disk cache — the same one a fuse mount has always had, which until then no other backend did. When the kernel does drop a file from its cache and asks for the bytes again, an unchanged file is copied from the local copy instead of downloaded (measured at ~57 MB/s against a ~90 ms download for a small file on a Pi CM4), and the version check that decides "unchanged" costs no request of its own. Two limits worth knowing, because they are what the win is not: the cache is bounded — an hour since last use, 1 GiB, and 100 MiB of free disk kept back — and the versions it remembers do not survive an agent restart, so the first read of each file after one downloads as before. Writes are unaffected: the share has no partial write, so a small edit to a large file is still a whole-file upload.
And from agent 0.4.68 the open is free too. Through 0.4.67 a warm re-read still spent about one request per file — not for the bytes, which were already local, but for the file's parent directory: every open stats it, and the agent looked for it only in the listing of the directory above it, which reading a folder's files never has a reason to fetch. So each open fell through to a lookup of a directory that had just been listed, measured at roughly 113 ms each on a real mount, which was nearly all of what a second pass over a tree still cost. A directory's own entry now comes back with its listing — the same request always carried it — so that stat is answered from what the node already holds. It expires with the listing it came from, on the same fs-cache-ttl clock, so nothing is served for longer than before, and a directory not covered by a warm listing is still asked about. A winfsp volume looks paths up through the same cache and gains the same thing; a fuse mount keeps its own directory cache and never paid this.
Appending to a file on an nfs mount needs agent 0.4.63 on Linux. Through 0.4.62 the shell's >> redirect emptied the file it appended to: the NFS server library the agent uses marks every ordinary create as truncating, and on a path that already existed the agent honoured it, so echo line two >> notes.md left the share holding line two alone — with every syscall reporting success. Agent 0.4.62 fixed one half of this (it stopped publishing an empty file the moment such a handle was opened) and 0.4.63 fixes the other. macOS was never affected, because its client checks the file exists before opening it and never asks for the truncation; Linux, where most nfs-backed nodes are, was. A client that genuinely does want the file emptied still gets that — it restates it as a separate resize, which the mount serves. If you are on an older agent, append over the share's URL or with the file tools until you can upgrade.
That upload happening on close is why agent 0.4.61 answers an attribute read from the file's unsaved changes while it is still open. Between a write and the upload, the router still holds the previous version, and the node used to answer from it — so a program that truncated or wrote through an open descriptor was told the size it had just changed, and the kernel's one-minute attribute cache then held that stale answer while the content was already correct. On 0.4.60 and earlier, truncate -s 8 file on the drive is the shape that shows it; close the file, or wait out the cache, and the size corrects itself.
A writer killed before it closes a file no longer strands what it wrote (agent 0.4.64). Because the upload happens on close, a kill -9 of a program writing to an nfs mount left the agent holding bytes it would never send: the mount went on answering that path's size and modification time from the abandoned copy — for every process on the node, outranking anything another node or the file tools wrote afterwards — until the agent restarted. The agent now saves those bytes itself once nothing has touched the handle for a minute, which is the POSIX answer: writes the program completed are what the file contains. It logs a warning naming the file when it does. A handle that is merely quiet is unharmed — it is flushed, not closed, and a writer that comes back can carry on writing to it — and if the save is refused because another node wrote the file in the meantime, the unsaved copy is set aside exactly as it would be on close. Unmounting the drive or dropping the connection already healed this case and still does.
From agent 0.4.59 an nfs mount also survives a restart of the agent. The server binds the same loopback port it bound last time, recorded per profile as the agent-written nfs-port key: the kernel remembers the port a mount was made against, so an agent that came back on a different one left the existing mount pointing at a port nothing was listening on, and every operation on it answered Operation timed out. If the remembered port is taken by something else, a new one is chosen, recorded, and the drive remounted. On 0.4.57 and 0.4.58, unmount the drive by hand after a restart that leaves it timing out.
Recording that port used to restart the agent, and agent 0.4.78 stops it. The agent watches its own profile file so that an edit you make is picked up without a restart — and through 0.4.77 it could not tell its own write from yours. Anything it recorded after startup therefore read as an edit and unwound the process for a reload: measured, the log said config changed, exiting for reload about five seconds after a mount came up, tearing down the drive that had just succeeded and costing the node a restart. It affected the values written from the mount and MCP paths — nfs-port, mcp-port, and last-mount-point in this release. The agent now recognises the write it just made and carries on; an edit of yours lands afterwards and is still picked up exactly as before.
The same release clears a mount left behind by a previous agent at the node's own mount point before mounting there. Mounting over a leftover stacks a second mount on top rather than replacing it — one extra volume per restart — and a wedged leftover holding the mount point made every new mount fail, leaving the node at mounted: false retrying indefinitely. Through agent 0.4.77 that was the only mount the agent would remove, because it is the one it can prove is its own — which is why fs: off left a stray mount alone until agent 0.4.78. Several layers can be stacked at once, so the clearing unmounts until the mount point is free, up to four layers.
On 0.4.59 that clearing ran after the mount point was created, which left the worst case unreachable: a wedged mount makes its own mount point unanswerable, so creating it failed first and the clearing never ran — fs.error read nfs: mount point /Volumes/nobgp: mkdir /Volumes/nobgp: file exists with fs.type empty, indefinitely. From 0.4.60 the clearing runs before the mount point is touched, so a node already stuck this way repairs itself on its next mount cycle. A failed mount that created the mount point also removes it again on the way out, since on macOS a leftover /Volumes entry reads to the next attempt as a mount point rather than as debris.
On macOS, agent 0.4.61 escalates an unmount that is refused. A macOS unmount answers Operation not permitted — to root, with nothing holding the volume — when something has objected to the volume going away, typically a Finder window on it or the "Server connections interrupted" dialog. That is not the same as busy, so umount -f does not override it and every retry failed identically, wedging the node with no way back. The agent now falls through to diskutil unmount force, which does override it, on both the shutdown path and the clearing above — so a node in this state clears the mount itself instead of needing a hand. Linux has no equivalent step and needs none: the lazy detach it already uses is the last escalation there.
A mount point that is busy but is no longer a mount says what is holding it, from agent 0.4.70. There is one shape the clearing above cannot help with: the old mount has already been unmounted, so there is nothing in the mount table to clear, and yet some process still has the directory open — so every remount answers Resource busy and the node retries that forever, reporting only those two words. The agent now names the holders in fs.error and in its log:
fs:
type: ""
mounted: false
error: "nfs: mount point /Volumes/nobgp is busy but not in the mount table — held by bash(4821), Finder(512), which must exit before the mount can recover: ..."
It names rather than reclaims, deliberately: a forced unmount cannot dislodge a live holder, and killing whatever is holding your mount point is not a decision the agent gets to make. Ending those processes is enough — the mount retry that is already running picks the drive back up on its next cycle with no restart. Where the holders cannot be identified (the lsof it uses is not installed, or the wedge defeats it) the message says so and still tells you the shape of the problem.
From agent 0.4.75 you get that answer whichever backend is mounting. The pin is on the mount point, which every backend on the platform shares, so switching backends was never an escape from it — and through 0.4.74 only an nfs mount said so. A Mac pinned to fs: webdav met the identical Resource busy with no diagnosis at all, and worse: a webdav mount failure is reported as a decline rather than an error, which clears fs.error, so such a node showed mounted: false with an empty reason indefinitely while the one sentence naming the process that had to exit went only to the local log. A busy point is now reported as the failure it is, in the same words, prefixed with the backend that hit it.
fuse joins them at agent 0.4.78. It leads the chain on Linux, so it is the backend most Linux nodes actually mount with — and through 0.4.77 a busy point there answered with generic advice to go and run fuser -m yourself, on a machine whose log you are usually reading from somewhere else. It now names the holders in the same words as the other two, both when a remount finds the point busy and when the unmount of a leftover is refused. The same release fixes a fuse mount point whose path contains a space: the kernel escapes it in its own mount table, and the agent's reading of that table did not undo the escaping, so such a point read as not mounted — which silently skipped both the leftover clearing and this diagnosis, on precisely the paths that are hardest to sort out by hand.
⚠ winfsp still reports nothing here, and not for want of trying: a refused WinFsp mount hands back no reason at all for the agent to pass on, and Windows has no lsof to name a holder with. On that backend a mount that will not come up still shows mounted: false with an empty fs.error.
Two machines on the same platform running the same agent can land on different backends, because the answer depends on what is installed. nobgp status reports which one was chosen and why the others were not:
fs:
type: fuse
mount: /mnt/nobgp
mounted: true
selected: auto
backends:
- "fuse: available"
- "nfs: unavailable (this kernel has no NFSv4 client (no `nfs4` in /proc/filesystems after a module load; OpenWrt: `opkg install kmod-fs-nfs-v4` matching the running kernel))"
- "webdav: available"
The list is that platform's preference order, best first, and holds only the backends that platform can run at all — a Mac lists nfs and webdav, a Windows node winfsp and webdav.
The backends list is reported even when nothing is mounted, which is when it is most useful — it is the answer to "which backend was this node even trying, and what is stopping it". From agent 0.4.59 an fs.error line sits beside it saying why there is no mount, when there is none — a pinned backend the host cannot serve, a failed mount, a backend that declined:
fs:
type: ""
mount: /Volumes/nobgp
mounted: false
selected: nfs
backends:
- "nfs: available"
- "webdav: available"
error: "nfs: mount_nfs -o port=61601,vers=4.0,...: exit status 1"
It is empty while the drive is mounted, and empty before the first attempt — absence means nothing has gone wrong yet, never that nothing is wrong.
Pinning a backend is the fs config key (auto, off, nfs, fuse, winfsp, webdav; default auto). It has no command-line flag — set it in the profile's YAML file (see Configuration file):
fs: nfs
- A pinned backend is honoured or refused, never substituted. Setting
fs: nfson a host with no NFS client fails out loud and retries the same backend — quietly mounting WebDAV instead would defeat both reasons to pin one (holding a backend steady, or diagnosing it). - A value that isn't one of the six is a typo, so it warns and falls back to
auto. A misspelling must not take a node's filesystem away. - A backend that
autoskips can still be pinned.winfspis unfinished rather than unavailable, so naming it is honoured — pinning is how a backend gets used beforeautowill take it. - Leaving the key out means
auto, so a node configured before the key existed keeps mounting exactly what it mounted. offmounts nothing — and serves nothing: the local shared drive proxy is not started either, so an operator who turns the drive off does not leave a loopback listener holding the node's credentials behind. It is also the escape hatch if the mount machinery itself misbehaves on a host. From agent 0.4.78 it clears a drive left behind by an agent that was killed, on Linux and macOS — see A mount left behind when the node is not going to mount; through 0.4.77 such a mount stayed until someone unmounted it by hand. A restart after an orderly stop is clean either way.- A mount failure never falls through to another backend. Selection falls back on a backend being unavailable, which is a property of the host; a failed mount is usually specific and transient, so the same backend is retried on the next cycle.
Changing fs takes effect on the next agent restart.
A mount left behind when the node is not going to mount
There are two ways a node starts up and never mounts anything: fs: off, and no mount point at all (a Windows profile that found no free drive letter). Through agent 0.4.77 both returned before looking at the machine's mount table — so an agent that had been killed while the drive was mounted (a SIGKILL, an out-of-memory kill, a host reset, a crash in the mount driver) and then came back configured not to mount left that mount in place, serving nothing. nobgp status said mounted: false beside a path the kernel still had a mount on, every read into it hung or errored, and the agent never touched it again — the way out was a person working out that the agent's report was not describing the kernel, and unmounting by hand.
From agent 0.4.78 the agent clears that mount at startup, on Linux and macOS:
- It asks the kernel, and matches the mount's source rather than its location. A mount is removed only when what the kernel says is mounted there is something this agent makes — its own FUSE filesystem, its own loopback NFS export, or its own local shared-drive proxy. Anything else at that path is somebody else's mount and is left where it is.
- Two paths are checked: the configured mount point, and where a mount was last actually up. The second is recorded in the profile as the agent-written
last-mount-pointkey, and it is what covers a mount point that was edited or blanked since the drive came up. It is never mounted on, and a stale value is harmless, because the source still has to match. - It does not escalate. Unlike the clearing on the mounting path, a refused unmount is not forced here — nothing is waiting on it, so it is left for the next start rather than pushed through against whatever is holding it. What it did, or could not do, goes to the agent's log.
- Windows is not covered yet. A leftover there is a drive mapping rather than a row in a kernel mount table; a Windows node running
fs: offstays silent about it rather than warning about a question that cannot be asked on that platform. Remove one withnet use <letter>: /delete.
An orderly stop — nobgp service stop, sudo nobgp service restart, a normal shutdown — has always unmounted cleanly on the way out, so this only ever mattered after a kill.
How long a directory listing is cached
The mount caches the listing of a directory it has read, so walking a tree does not ask noBGP about the same directory once per file in it. From agent 0.4.66 how long it may serve that copy is yours to set, as the fs-cache-ttl config key (or NOBGP_FS_CACHE_TTL, or --fs-cache-ttl on a foreground nobgp agent). The default is 30s, and the value is clamped to [0, 10m]:
fs-cache-ttl: 2m
- It is a staleness window, not a performance dial. It is exactly how long this node may keep showing a directory as another node left it before that node's newer write — and the same number is how often every mount re-asks about a directory nothing changed. Both directions cost something.
0is a real setting, not "off". It re-asks every time, while file contents are still served from the local cache when the version has not moved — the freshness answer for a tree several machines write to at once.- Write a unit.
fs-cache-ttl: 30is thirty nanoseconds, not thirty seconds, so a value under a second is treated as a missing unit and ignored with a warning rather than obeyed. Write30s. Anything that is not a duration at all, and anything past the ten-minute ceiling, is likewise reported and clamped rather than taking the node somewhere nobody asked for. - From agent 0.4.67 it is the whole answer on
nfs,fuseandwinfsp. In 0.4.66 it reached thenfsbackend alone, whose own listing cache was a fixed 60 s before that; afusemount kept a separate hard-coded 30 seconds and awinfspone cached nothing. All three now read this key, and where the kernel caches in front of the agent — annfsmount's attribute cache, afusemount's directory entries — that window comes from the same number, so the two caches in series cannot disagree. When they did, the longer one won while everything claimed the shorter: annfsnode set to5sstill let the Linux kernel serve a directory attribute up to 30 seconds old, because the floor it mounts with defaults to 30. Awebdavmount is the operating system's own client and still caches on its own terms. - The kernel's half is fixed when the drive is mounted. On
nfsandfusethe timeouts handed to the kernel are mount options, and a mount option cannot be changed under a live mount — so a changedfs-cache-ttlreaches the agent's own cache on the config reload and the kernel's on the next mount cycle. Restart the agent if you want both at once. - It does not govern file contents. Whether a cached copy of a file may be served is decided by comparing versions with the router, not by this clock, so raising it does not make a mount serve stale bytes for longer.
Unlike the other flags, --fs-cache-ttl is not written to the config file by nobgp config — to make a value durable, put it in the profile's YAML (the agent reloads on the edit) or in the environment.
One related behaviour that is not tunable: if the refresh of an expired listing fails — a router blip, a moment without a network — the copy the node already had is served for up to 30 seconds past its TTL rather than reporting an I/O error or, worse, an empty directory. Past that the error is real.
A momentary busy answer from noBGP no longer reaches the program as an I/O error, from agent 0.4.77. A short burst of 503s — a gateway with no healthy target for an instant, one refusing new work, one that answered too slowly, or a rate limit (429, 502, 503, 504) — used to surface on the mount as ls: fts_read: Input/output error, which reads as corruption for a condition that means come back shortly. Measured on a Mac: 35 of them in 16 seconds. The mount now retries such a request itself, and because the retry lives in the one client every backend shares, nfs, fuse, winfsp and webdav all get it.
- Bounded, deliberately short. All attempts of one operation share about 5 seconds, and never outlive whatever deadline the calling program already had. It does not paper over a real outage — holding a filesystem call open for the length of one would trade an honest error for a process you cannot kill — so a longer outage still reaches you as an error, with the node's log naming the status and how many attempts it spent.
- The pause is the server's if it says so. A
Retry-Afterthat fits the budget is honoured; one that does not ends the retrying rather than being shortened to something noBGP never agreed to. Otherwise the wait backs off from 100 ms with jitter, so the many files a directory listing opens at once do not come back in lockstep. 500is never retried. That one is a failure handling this particular request, and repeating it only turns a bad request into a loop.- Uploads are not retried here, because a request body cannot be replayed. Writes are already covered where it counts: a queued write-back retries on its own schedule and survives a restart. What still reaches you is an upload a program is waiting on — a
close()onnfs, a flush onwinfsp— and that is the honest answer rather than a gap.
Writing a lot of small files stopped throwing the listing away, in agent 0.4.70. Every completed upload used to discard the cached listing of the file's directory, so a burst of writes left the mount with no warm listing at all and every subsequent open paid its own round trip to ask about one file. Measured on a winfsp volume: 100 reads of 4 KB files took 60 seconds, against 1.1 s for the same files through the Windows WebDAV drive. Two changes fix it, both in the cache the nfs, fuse and winfsp backends share. An upload now edits its own row into the listing — the name it wrote, the size it sent, the version the router answered with — instead of forgetting the whole directory; and a question about a file the node has no answer for lists the parent directory, one request, which warms every file beside it for the TTL rather than answering about one. Nothing is served for longer than before: the edited row expires with the listing it joined, on the same clock, and a name a listing does not hold is still asked about individually rather than reported as missing. On 0.4.70 the nfs backend got the second half but not the first — its own uploads still dropped the parent listing, so a write-heavy burst there cost what it always had; from agent 0.4.73 the edit happens inside the upload itself, which is the one door every backend's writes go through, so nfs gains it too. Writing 60 files into a directory no longer throws that directory's listing away 60 times, and reading them back afterwards costs no request per file.
A fuse mount also stops inventing a change every time it fetches a file (agent 0.4.70). A file's size and modification time were answered from the node's local copy whenever one existed, and a local copy's timestamp is when the node downloaded the file rather than when anyone wrote it — so an unchanged file looked freshly modified after every fetch, which defeats the attribute cache the Linux kernel keeps in front of the mount and the re-download suppression that rides on it. The local copy now answers only while it is the only place the bytes exist: an open handle holding writes, or an upload still queued or in flight. Everything else is answered from what noBGP holds. It is the same rule nfs has followed since 0.4.61 and winfsp since 0.4.69, and a rename carries it with the file so a renamed file with unsaved bytes still reports its own size.
fuse or nfs mount enforces a lock between two nodesFrom agent 0.4.64 a Linux fuse mount forwards the locks taken on it to noBGP, so flock and fcntl there hold between nodes and not only between the processes on one machine; from agent 0.4.69 an nfs mount does the same, on Linux and macOS both — see Locks on a fuse or nfs mount below. On the remaining backends a lock is at best local to the machine holding it: nothing coordinates two nodes writing the same file on the share, so a lock that works perfectly while you test on one box buys nothing the moment a second node touches the file, which is the way this misleads hardest.
Which of these a node lands on moved twice in as many releases (see the preference order): from 0.4.65 a Linux node with FUSE lands on a backend where a lock does hold, and a Mac spent that release on webdav — where flock is told it succeeded and enforces nothing — before 0.4.66 put it back on nfs, which refused a lock out loud then and enforces it between nodes from 0.4.69. A Mac pinned to fs: webdav stays on the backend that lies.
Even where the lock does hold between nodes, keep SQLite and anything else that keeps a file open and writes in place off the share. The drive uploads a whole file when it is closed and serves reads from a local copy, so the data path is wrong for that shape of program whatever the locking says.
What each backend does, as measured:
| Mount | What a lock does |
|---|---|
nfs, agent 0.4.69+ | Enforced between processes on the machine and between nodes — noBGP arbitrates it, through the same lease a fuse mount takes. Linux and macOS both. A lock it cannot forward is refused rather than granted. See below. |
nfs, agent 0.4.68 and earlier | Not implemented, and fails loudly rather than pretending. Nothing that locks before writing is protected — and nothing is misled either. On macOS that was a straight improvement on WebDAV. |
fuse, agent 0.4.64+ | Enforced between processes on the machine and between nodes — noBGP arbitrates it. A lock it cannot forward is refused rather than granted. See below. |
fuse, agent 0.4.63 and earlier | Enforced between processes on that machine and not between nodes. Measured on two nodes holding the same file at once, 2026-08-07. |
webdav on macOS | Reports success and enforces nothing. Two processes each took an exclusive flock on one file and both were told they held it; fcntl refuses cleanly, flock is the one that lies. Measured on /Volumes/nobgp, 2026-08-07. |
webdav on Windows | An exclusive open is enforced between local processes and reported honestly — and that is the trap. It does not exclude a writer on another node, and a held handle's write-back on close overwrites theirs, producing a file that is neither version with success reported to both. Measured 2026-08-08. |
webdav on Linux | Unmeasured. Assume nothing is enforced and that a failure may be silent. |
winfsp | An exclusive open is enforced between processes on the machine and reported honestly — and that is the trap: it does not exclude a writer on another node. Unlike this platform's WebDAV mount, a losing cross-node write is refused rather than blended into the other version — every write-back is conditional on the version the handle read, and the rejected bytes are kept. Measured 2026-08-11. |
From router 0.4.62 you can read this per node instead of looking it up here: network_directory reports it as info.fs_locking, derived from the node's backend, platform and — from router 0.4.68 — its agent version. That last part is what makes the table above readable per node: an nfs mount reads cluster from agent 0.4.69 and none below it, a fuse mount reads cluster from 0.4.64 and node below it, so a node that has not upgraded yet is described as what it actually does rather than as what your newest node does. A node whose agent version the router cannot read is described as pre-forwarding, never as cluster. A winfsp node reads node from router 0.4.71 — before that it was the one backend with no row, so the field was absent there and silence read as "no answer" rather than "no guarantee". Its row is not version-aware like the other two: the backend has nothing to forward a lock with, so node is the floor rather than a claim that improves with the agent.
If you need a lock on a backend that does not hold one, take it through the file tools rather than on the mount: file with op: "lock" locks a path on the share itself, where noBGP arbitrates it. It is advisory — it coordinates callers that also lock, and does not stop a program writing through the mount. It is the same lease a fuse mount on agent 0.4.64+ and an nfs mount on agent 0.4.69+ take, so all three arbitrate against each other on the same path.
If you write over HTTPS rather than through the mount, you can also make the write itself conditional and have a stale one refused rather than accepted — see Conditional writes.
Locks on a fuse or nfs mount
From agent 0.4.64 a Linux fuse mount stops letting the kernel arbitrate locks by itself and hands them to noBGP instead, which is what makes them mean something on a second node. From agent 0.4.69 an nfs mount does the same, on Linux and macOS alike. flock and fcntl byte-range locks both go through it; nothing in a program has to change.
The two backends share one lease manager inside the agent deliberately, so they cannot drift into two renewal cadences or two ideas of when a lock has been lost. Everything below holds on both unless a bullet says otherwise.
Two layers do the work, and they are deliberately different shapes:
- On the machine, the agent keeps a real byte-range table — per path, per lock owner, shared and exclusive distinguished — so everything the kernel used to do locally still holds, including disjoint ranges and shared read locks.
- Between nodes, noBGP holds one whole-file exclusive lease per path, taken while this node holds any lock on the file and dropped when it holds none. That is coarser than what the program asked for, always in the safe direction: a shared read lock excludes a remote reader it need not have, and two disjoint ranges on two machines contend. It never grants where a local lock would refuse.
Things worth knowing before relying on it:
- The node is the holder, not the process. noBGP names the lease from the node's connection, so every process on one machine is one holder upstream, and re-taking a path this node already holds renews rather than contends. It is the local table that keeps two processes on the same box apart.
- The lease is the same one the file tools take. A path locked on a mount is contended for a caller using
filewithop: "lock", and the other way round. - A blocking
fcntl(F_SETLKW)behaves differently on the two backends. Onfuseit waits up to 30 seconds and then returnsEINTR, which means "ask again" — a caller that wants to keep waiting reissues the call, and the agent polls with a backoff on your behalf. Onnfsthe kernel's own client does the retrying, so a blocking wait simply blocks. noBGP holds no queue of waiters either way, and a signal interrupts the wait immediately, as it would on a local filesystem. F_GETLKasks noBGP too, so it can report a file another node holds. A remote holder is described as a whole-file exclusive lock belonging to no process on your machine — onfusethat is spelled pid 0, and onnfsthe holder carries no client identity at all. The lease has no range and no process behind it, so neither answer can be mistaken for a local one.- A lock that cannot be forwarded is refused, never granted. On
fusethe refusal isENOLCK("no locks available") rather thanEAGAIN; onnfsa contended path is denied outright, a momentary rate limit is retried by the kernel client without your program seeing it, and anything permanent surfaces as an I/O error with the agent logging the path. A mount never grants a lock it cannot enforce, so a program that checks its return value stops rather than proceeding unprotected. - A lease that is lost surfaces on the next write. The agent renews every 20 seconds; if renewal is refused because another node took the path, or fails for longer than a minute, the lock is gone — and since neither mount has a way to announce it,
write,fsyncandcloseon the handles that held it answerEIO. Reads keep working. The agent logs the file it happened to. - Locks are released when the file is closed and when the drive unmounts, so a process that exits holding one does not leave the path locked for the rest of its two-minute expiry.
Reaching a share directly
Every network's share also answers over HTTPS as a WebDAV endpoint, keyed on the network's id (router 0.4.54+):
https://files.nobgp.com/networks/<network-id>/
PROPFIND lists a directory, GET downloads a file, PUT uploads one, and MKCOL, MOVE, COPY and DELETE manage the tree. Authenticate with your noBGP bearer token; the URL carries exactly the access your account already has to that network, so there is nothing extra to grant. You never have to assemble it by hand — ask your AI assistant for the network directory and each network comes back with its own URL as files_url.
It is keyed on the network id rather than its name deliberately: the same name can exist in more than one organization you belong to, so a name would not say which share you meant.
Conditional writes
Every file on the share carries an ETag, and from router 0.4.63 you can make a PUT conditional on it so a write that would clobber someone else's is refused instead of silently winning.
# Read the file and its validator
curl -si -H "Authorization: Bearer $NOBGP_TOKEN" \
https://files.nobgp.com/networks/<network-id>/notes.md
# ...
# ETag: "1f4a2-17c9d3e8b2a4c00-3e8"
# Write it back only if nobody changed it in the meantime
curl -X PUT -H "Authorization: Bearer $NOBGP_TOKEN" \
-H 'If-Match: "1f4a2-17c9d3e8b2a4c00-3e8"' \
--data-binary @notes.md \
https://files.nobgp.com/networks/<network-id>/notes.md
- The same
ETagcomes back fromGET,HEAD,PROPFINDand thePUTresponse, so whichever way you read a file, the validator you hold is the one a laterPUTis compared against. It is an opaque string — never parse it — and it changes whenever the file's bytes change. If-Match: "…"refuses a stale write with412 Precondition Failed. Read the file again, merge, and retry.If-Match: *requires the file to exist — useful when you mean to replace something and not to create it.If-None-Match: *is an exclusive create: thePUTsucceeds only if nothing is there, and comes back412if there is.- A
PUTthat sends neither header behaves exactly as it always has, and is accepted whatever the file looks like now. Conditional writing is something a client of your own opts into. - The condition is checked twice, and the second check is the one that decides (router 0.4.76). It is read once when the request arrives, which refuses a doomed write before you spend the upload on it, and again immediately before the upload is published. An upload takes as long as its body — seconds on a large file — and through router 0.4.75 anything that landed inside that window was overwritten anyway, with success reported to both writers. That upload is refused with
412now, andIf-None-Match: *claims the path against other uploads in flight rather than only against the requests that had already finished. - It narrows the window rather than closing it. What is left is the gap between that last check and the moment the file becomes visible — microseconds rather than the length of a body. Closing it entirely would need a form of rename the operating system does not offer.
This is not the WebDAV If: header, which carries lock tokens; these are the ordinary HTTP conditional headers.
The mounted drive uses it too
From agent 0.4.62 the mounted drive conditions its own write-backs the same way, so the lost update above is refused on the mount rather than only in a client you write yourself. It needs a router on 0.4.63 or later; against an older one the write goes out unconditional exactly as it did before. The winfsp backend joins from agent 0.4.63, which is the release where it started writing at all.
The shape it protects against is the everyday one: the mount uploads a whole file when it is closed, so an editor that keeps a file open and saves its whole cached copy each time would otherwise destroy anything another node wrote in between, with success reported to both.
nfs— from agent 0.4.63 the refusal reaches the program on itswrite, and that is the fix that makes the rest of this true on Linux. Through 0.4.62 the bytes were correctly refused but the error was raised atfsyncorclose, and the NFS server library the agent used then discarded the error from both — so the losing writer was told its save had landed after all. The handle now re-checks the version on its way to its first write (one extra round trip per handle, and only for a handle that read the file and is now writing it), latches the refusal, and hands it back from every laterwriteon that handle. It narrows the window rather than closing it: a write that lands in the milliseconds between that check and the upload is still refused by the router, and that refusal still arrives where nothing can carry it. The file stays unsaved on the node, and its unsaved bytes are kept, not deleted — set aside next to the agent's cache, with the log naming the path (agent 0.4.63; before that a refusednfswrite-back discarded its local copy). From agent 0.4.69 a refusal also reachesfsync, which the replaced server is what makes possible — until then only a laterwriteon the same handle could tell you. From agent 0.4.70 the copy is set aside in the same place every other backend uses — beside the cached file, with.rejectedon the end — so hunting for a rejected save needs no knowledge of which backend the node mounted with.fuse— the upload is asynchronous, sowritehas usually already returned by the time the router refuses. The nextfsyncorcloseon that file gets a hard write error instead of another silent success, the mount goes back to showing the router's version, and the rejected copy is kept, not deleted — the agent log names the file it was set aside as.webdav— unchanged. The agent's local proxy forwards anIf-Matchits caller sends but never adds one, and the operating system's own WebDAV client does not send one, so a Windows or macOS WebDAV mount still writes unconditionally.winfsp— from agent 0.4.63, when the backend writes at all, and in practice from 0.4.65, when a real mount started accepting writes. Through 0.4.66 the write-back happened inside theCloseHandlethat triggered it, so a refused save failed that call. From agent 0.4.67 the upload is asynchronous likefuse's, so the close has usually already reported success by the time the router refuses: the refusal is latched onto the handle and handed back from the next flush,fsyncor close on that file, and the unsaved bytes are set aside beside the cache with the log naming the path. In 0.4.62 the backend was read-only and nothing was written from the mount.
A queued upload survives the agent restarting, from agent 0.4.75. On a fuse or winfsp mount the upload is deferred by a few seconds so several quick saves cost one transfer — and through agent 0.4.74 a restart inside that window dropped the save, silently, after the program that made it had already been told it succeeded. Restarts are routine: an auto-upgrade or a config reload is enough. It got worse from there, because the node's cache dates a copy it adopts from a previous process by the file's modification time, so an hour-old unsaved save was already past the cache's one-hour age limit the moment the agent came back — and the cleaner deleted the only copy of it.
The queue now records each pending upload on disk beside the bytes, at the moment the write is queued rather than at shutdown, so a kill -9 loses no more than an orderly stop does. The next start re-queues them and logs how many. Until the bytes are upstream the copy holding them is exempt from the cleaner and from the cache's size limit — an unsaved save is never what gets evicted to make room, and the limit is exceeded and reported instead. A resumed upload goes out unconditionally, like any copy adopted from a previous process. An nfs mount uploads on close rather than queueing, so it was never exposed to this.
Some writes still go out unconditionally on every backend, because there is no version to demand: a file opened with O_TRUNC (the caller named an existing file to replace), a file cached before the agent last restarted (the validators do not survive a restart), and — on nfs from agent 0.4.65, where the contents are only fetched when something asks for them — a handle that reached its first write without ever having read the file. It is one rule in three shapes: a caller that read nothing cannot be overwriting anything it read.
None of this makes a mount safe for two nodes editing one file — it turns a silent loss into a visible error. For coordination that actually holds between nodes, lock the path through the file tools, or lock it on the mount itself from a Linux fuse node on agent 0.4.64+ or an nfs node on agent 0.4.69+, both of which take the same lease.
Two nodes creating one file
A create used to be on that list of unconditional writes, and from agent 0.4.70 it is not. The old reasoning was the same rule — a caller that read nothing cannot be overwriting anything it read — and it is wrong for exactly one shape, measured on two nodes on 2026-08-11: both create the same path inside the write-back window, neither read anything because nothing existed in either one's view, and the loser silently destroyed bytes the winner had already been told were saved.
A create now claims the path exclusively (If-None-Match: *, the same conditional write a client of your own can make), so the second one loses out loud instead of quietly winning:
- On a
fuseorwinfspmount, and for any file the node's cache is managing, the refusal takes the same path a stale version does: the copy is set aside next to the cache with the log naming it, the refusal is latched onto the handle, and the nextfsync, flush or close reports it rather than another silent success. Onwinfspa create that loses answersEEXIST, which Windows turns into whatever the caller asked for next — an overwrite-minded program retries as an open-and-truncate, which is a different question and is answered normally. - On an
nfsmount the placeholder a create publishes is exclusive too.O_EXCL— the guarded create — now fails across nodes rather than only against the processes on this machine, which is whatO_EXCLis for; an ordinaryopen(O_CREAT)that finds the peer got there first simply opens the peer's file, which is what that call means on a path that exists. - Renaming a freshly created file keeps the claim with it, so the write-a-temp-then-rename pattern most editors use is covered.
It needs a router on 0.4.63 or later, the release that honours the header. Against an older one the create degrades to the unconditional write it was before, silently — the same compatibility rule the version check itself follows. Router 0.4.76 is what makes the claim hold between two uploads in flight: before it, the exclusive claim was settled when the request arrived, so two nodes that both started a create while neither had published still ended with one overwriting the other. The claim is now re-checked as the upload is published, which is the moment it has to be true. One narrow case stays uncovered: if something else on the same node reads the path in the window between the create and its upload, the write is conditioned on the version that read found rather than on the path being empty, so it composes with the peer's file instead of refusing. That is still strictly better than the clobber it replaced.
Each node also has a storage area of its own, separate from every network's share:
https://files.nobgp.com/nodes/<node-id>/
It follows the machine rather than a network — a node that joins several networks still has exactly one area, belonging to none of them — and it is not a network's shared drive. On the machine itself it is the node/ folder on the mount (agent 0.4.54+, see What the mount contains), which needs no URL and no token. Reaching it over HTTPS needs the Owner or Admin role in the organization that owns the node, a step above the network share, because nothing the node's owner sets on the box filters writes that arrive this way. A node always reaches its own area; from router 0.4.73 it reaches a peer's only when it holds the manage tier and the peer is in its own network.
It outlives the node, for a month. Deleting a node does not delete its storage there and then — the area and everything in it stay, reachable by the URL above, so a machine that is rebuilt or replaced does not silently take its files with it. From router 0.4.73 that grace has an end: 30 days after the node is deleted the area is queued for deletion, and the bytes go about a week after that — so nothing is removed sooner than roughly five weeks from the deletion, and after that the area is gone for good. Copy out anything you want to keep inside that window; delete the contents yourself sooner if you are done with them. A deleted node's area stops counting toward your storage usage from the moment the node goes, so the retained files are never billed for.
Bringing the node back inside the window calls the deletion off, and the area comes back with the node's identity, contents intact. Past the window the identity, labels and role grants still return and the area is empty.
A deleted network's shared drive is kept and swept on the same clock — 30 days, then queued — with the difference that a network has no way back once deleted, so treat its deletion as final for its files too.
A node's area counts toward your storage allowance while the node exists (router 0.4.61+), billed to the organization that owns the node and pooled with that organization's network shares — see Storage.
⚠ A node that is rebuilt from scratch gets a NEW area, not its old one. The area follows the node's identity, which survives a reinstall that re-uses the existing registration but not a fresh enrolment — so a replacement machine starts empty, and the predecessor's files remain where they were, reachable by the URL above.
Your AI assistant can reach both without a WebDAV client (router 0.4.56+). The same file tools it uses on a node's own disk now address these two places directly: name a network and no node for its shared drive, or a node with storage: true for that node's area. The router answers from its own storage, so nothing has to be online — you can read a machine's files while it is switched off, and put files there for it to find when it comes back. Each call moves at most 1 MiB: a larger file can be read a megabyte at a time (router 0.4.59+), but writing one still means a PUT to the URLs above. Details in the MCP reference.
Using File Sharing with Your AI Assistant
Your AI assistant can:
- Point you to your network's files in the dashboard (Networks → your network → Files)
- List files and directories currently stored
- Guide you on uploading or downloading files
- Help you organize and manage your shared files
The nobgp file CLI commands (upload, download, ls, rm) let you transfer files directly from any node. Your AI assistant can run these on your behalf using the command tool. You can also manage files in the web dashboard under Networks → your network → Files.
Use Cases
- Share configuration files across nodes - Upload once, access from all nodes
- Distribute deployment artifacts - Share builds or packages with your infrastructure
- Collect logs from multiple machines - Centralized storage for log files
- Transfer files between disconnected systems - Bridge air-gapped environments
Example Workflow
You: I need to share a config file with all my production servers
AI: I can help you with that. You can upload your config file to the shared drive
for your production network.
Open app.nobgp.com, go to Networks, choose "production", then Files.
Once uploaded, the file will be available at /mnt/nobgp/networks/production/
on all nodes in your production network.
You: How do I access it from my servers?
AI: On any node in the production network, the file will be at:
/mnt/nobgp/networks/production/your-config-file.conf
You can copy it to the appropriate location, for example:
cp /mnt/nobgp/networks/production/your-config-file.conf /etc/app/config.conf
Web Dashboard
The noBGP web dashboard provides a graphical interface for managing your account:
- Network management: Create and configure networks
- Node overview: See all connected nodes and their status
- Registration keys: Generate and manage keys for automated agent enrollment
- Service management: View and configure published services
- File sharing: Upload and download a network's files under Networks → your network → Files
The web dashboard complements your AI assistant — use it for account management tasks and visual overviews, while using the AI assistant for operational tasks like running commands and troubleshooting.
Putting It Together
Here's how these concepts work together in a typical workflow:
The flow:
- You authenticate to noBGP via your AI assistant (OAuth)
- Your request is authorized based on your account permissions
- Operations execute within your networks (isolated from other users)
- Nodes authenticate via registration (OAuth or key) and then JWT tokens
- All communication is encrypted and secured
- Results stream back to your AI assistant in real-time
Next Steps
Now that you understand the core concepts:
- Try Your First Steps - Hands-on walkthrough of common tasks
- Install an Agent - Connect your first machine
- Provisioning Guide - Create nodes on-demand
- Service Publishing Guide - Expose your applications
- Monitoring & Events - Subscribe to file changes, fleet-wide commands, and node presence
Questions?
Q: How many networks can I have? A: As many as your plan allows — see Plans & Billing. Create separate networks for different environments (dev, staging, prod).
Q: Can nodes be in multiple networks? A: Yes! A node can join multiple networks simultaneously using multiple agent profiles (see Multi-Profile Support)
Q: What happens if I regenerate my registration key? A: Already-registered nodes are unaffected — they use JWT tokens. Only new registrations using the old key will stop working.
Q: Are services publicly accessible? A: Only if you make them public. By default, all services require OAuth authentication.
Q: How long do sessions stay alive? A: Sessions have a 1-hour idle timeout. After the command exits, there is a 30-second grace period to retrieve the final exit code.