Advanced Backends
Section titled “Advanced Backends”Hoody’s VFS exposes a set of advanced backends — alias, cache, chunker, combine, hasher, local, memory, and union — alongside the persistent FUSE mount lifecycle that exposes them on disk. Use the endpoints on this page to connect specialized backends, list and inspect mounts, create new persistent mounts, retune VFS cache parameters, and tear mounts down.
All endpoints on this page are served from the container’s files service: https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com.
Mounts
Section titled “Mounts”Mounts are persistent FUSE filesystems that surface a connected backend on disk. Every mount survives container restarts; to remove one, call the unmount endpoint.
GET /api/v1/mounts
Section titled “GET /api/v1/mounts”List all mounts. Optionally filter by exact label.
This endpoint takes no path parameters.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
label | query | string | No | Filter mounts by label. Only mounts with this exact label will be returned. |
curl -X GET "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts?label=Photos" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.list("Photos");{ "count": 2, "mounts": [ { "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "b_8f4a2c1e9b3d4a5f6c7e8d9a0b1c2d3e", "label": "Photos", "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "active", "created_at": 1716200000 }, { "id": "mount_660f9511f30c52e5b827557766551111", "backend_id": "b_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", "label": "Photos", "mount_path": "/hoody/mounts/mount_660f9511f30c52e5b827557766551111", "status": "active", "created_at": 1716203600 } ]}GET /api/v1/mounts/{id}
Section titled “GET /api/v1/mounts/{id}”Get detailed information about a specific mount, including its VFS cache configuration.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Mount ID. |
curl -X GET "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.getDetails("mount_550e8400e29b41d4a716446655440000");{ "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "b_8f4a2c1e9b3d4a5f6c7e8d9a0b1c2d3e", "label": "Photos", "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "active", "created_at": 1716200000, "vfs_config": { "cache_mode": "writes", "cache_max_age": 3600, "cache_max_size": 10737418240, "dir_cache_time": 300 }}POST /api/v1/mounts
Section titled “POST /api/v1/mounts”Create a persistent FUSE mount for a connected backend. The mount is automatically persisted and restored on server restart; there is no separate persist flag.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
backend_id | string | Yes | — | ID of an existing backend connection. |
label | string | No | — | Optional human-readable label (e.g., "My NAS", "Photos Backup"). Used by the UI and to filter mounts via GET /api/v1/mounts?label=.... |
mount_path | string | No | — | Path for the mount. If omitted, defaults to /hoody/mounts/mount_{uuid}. Relative paths are resolved under the server’s mount directory (/hoody/mounts/). |
vfs_config | object | No | — | VFS configuration for performance tuning. See below. |
The vfs_config object accepts the following fields:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
vfs_config.cache_mode | string | No | "writes" | Cache mode. One of off, minimal, writes, full. |
vfs_config.cache_max_age | integer | string | No | 3600 | Maximum time files are cached. Accepts seconds or duration strings like "1h", "30m". |
vfs_config.cache_max_size | integer | string | No | 10737418240 | Maximum cache size in bytes (default 10GB). Accepts bytes or human-readable strings like "10G", "128M". |
vfs_config.dir_cache_time | integer | string | No | 300 | How long directory listings are cached. Accepts seconds or duration strings like "5m", "1h". |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "backend_id": "b_8f4a2c1e9b3d4a5f6c7e8d9a0b1c2d3e", "label": "Photos Backup", "vfs_config": { "cache_mode": "writes", "cache_max_age": 3600, "cache_max_size": "10G", "dir_cache_time": "5m" } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.create({ backend_id: "b_8f4a2c1e9b3d4a5f6c7e8d9a0b1c2d3e", label: "Photos Backup", vfs_config: { cache_mode: "writes", cache_max_age: 3600, cache_max_size: "10G", dir_cache_time: "5m" }});{ "success": true, "message": "Mount created successfully", "data": { "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "b_8f4a2c1e9b3d4a5f6c7e8d9a0b1c2d3e", "label": "Photos Backup", "mount_path": "/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "active" }}PATCH /api/v1/mounts/{id}
Section titled “PATCH /api/v1/mounts/{id}”Update the VFS configuration for an existing mount. Allows changing cache settings, buffer sizes, and other VFS parameters.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Mount ID. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
vfs_config | object | Yes | VFS configuration parameters. |
curl -X PATCH "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "vfs_config": { "cache_mode": "full", "cache_max_age": 7200, "cache_max_size": "20G", "dir_cache_time": "10m" } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.update("mount_550e8400e29b41d4a716446655440000", { vfs_config: { cache_mode: "full", cache_max_age: 7200, cache_max_size: "20G", dir_cache_time: "10m" }});{ "success": true, "message": "Mount configuration updated"}{ "success": false, "error": "Invalid cache_mode value: must be one of off, minimal, writes, full"}{ "success": false, "error": "Mount not found"}DELETE /api/v1/mounts/{id}
Section titled “DELETE /api/v1/mounts/{id}”Remove a mount and disconnect the FUSE filesystem.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Mount ID. |
curl -X DELETE "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.unmount("mount_550e8400e29b41d4a716446655440000");{ "success": true, "message": "Mount removed successfully"}{ "success": false, "error": "Mount not found: mount_550e8400e29b41d4a716446655440000"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_NOT_FOUND | Mount not found | No mount exists with the specified ID | Verify the mount ID is correct or list all mounts |
Backends
Section titled “Backends”Connect specialized backends to expose new filesystem capabilities. Each endpoint returns a backend ID that can later be mounted via POST /api/v1/mounts.
POST /api/v1/backends/alias
Section titled “POST /api/v1/backends/alias”Alias for an existing remote or local path. Useful for renaming or remapping an existing mount target.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
remote | string | Yes | "" | Remote or path to alias. Can be "myremote:path/to/dir", "myremote:bucket", "myremote:" or "/local/path". |
description | string | No | "" | Description of the remote. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/alias" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "remote": "myremote:archive/2023", "description": "Aliased view of the 2023 archive directory" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectAlias({ remote: "myremote:archive/2023", description: "Aliased view of the 2023 archive directory"});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_a1b2c3d4e5f60718293a4b5c6d7e8f90", "type": "alias", "backend_type": "alias", "mount_paths": [] }}{ "success": false, "error": "remote must be a non-empty string in the form remote:path or /local/path"}POST /api/v1/backends/cache
Section titled “POST /api/v1/backends/cache”Cache a remote locally to speed up reads and reduce API calls against the upstream provider.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
remote | string | Yes | "" | Remote to cache. Normally should contain a : and a path, e.g. "myremote:path/to/dir", "myremote:bucket", or "myremote:" (not recommended). |
description | string | No | "" | Description of the remote. |
chunk_clean_interval | integer | No | 60 | How often (in seconds) the cache performs cleanups of the chunk storage. |
chunk_no_memory | boolean | No | false | Disable the in-memory cache for storing chunks during streaming. |
chunk_path | string | No | "/home/user/.cache/hoody-vfs/cache-backend" | Directory to cache chunk files. |
chunk_size | string | No | "5242880" | Size of a chunk. One of "1M", "5M", "10M". |
chunk_total_size | string | No | "10737418240" | Total size the chunks can take up on the local disk. One of "500M", "1G", "10G". |
db_path | string | No | "/home/user/.cache/hoody-vfs/cache-backend" | Directory to store file structure metadata DB. |
db_purge | boolean | No | false | Clear all the cached data for this remote on start. |
db_wait_time | integer | No | 1 | How long to wait (in seconds) for the DB to be available. 0 is unlimited. |
info_age | integer | No | 21600 | How long (in seconds) to cache file structure information. One of 1h, 24h, 48h. |
plex_insecure | string | No | "" | Skip all certificate verification when connecting to the Plex server. |
plex_password | string | No | "" | The password of the Plex user. |
plex_token | string | No | "" | The plex token for authentication (auto set normally). |
plex_url | string | No | "" | The URL of the Plex server. |
plex_username | string | No | "" | The username of the Plex user. |
read_retries | integer | No | 10 | How many times to retry a read from a cache storage. |
rps | integer | No | -1 | Limits requests per second to the source FS. -1 disables. |
tmp_upload_path | string | No | "" | Directory to keep temporary files until they are uploaded. Specifying a value enables this feature. |
tmp_wait_time | integer | No | 15 | How long (in seconds) files should be stored in local cache before being uploaded. |
workers | integer | No | 4 | How many workers should run in parallel to download chunks. |
writes | boolean | No | false | Cache file data on writes through the FS. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/cache" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "remote": "s3remote:media-bucket", "description": "Cached view of media bucket", "chunk_size": "5M", "chunk_total_size": "10G", "workers": 8, "writes": true }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectCache({ remote: "s3remote:media-bucket", description: "Cached view of media bucket", chunk_size: "5M", chunk_total_size: "10G", workers: 8, writes: true});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_c4d5e6f70819293a4b5c6d7e8f90a1b2", "type": "cache", "backend_type": "cache", "mount_paths": [] }}{ "success": false, "error": "remote must be specified"}POST /api/v1/backends/chunker
Section titled “POST /api/v1/backends/chunker”Transparently chunk and split large files on a remote, with optional per-chunk checksums.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
remote | string | Yes | "" | Remote to chunk/unchunk. Normally should contain a : and a path. |
description | string | No | "" | Description of the remote. |
chunk_size | string | No | "2147483648" | Files larger than chunk size will be split into chunks. |
fail_hard | boolean | No | false | Choose how chunker should handle files with missing or invalid chunks. |
hash_type | string | No | "md5" | Hash mode. One of none, md5, sha1, md5all, sha1all, md5quick, sha1quick. All modes but "none" require metadata. |
meta_format | string | No | "simplejson" | Format of the metadata object or "none". |
name_format | string | No | "*.hoody-vfs_chunk.###" | String format of chunk file names. Must contain exactly one * and one or more consecutive #. |
start_from | integer | No | 1 | Minimum valid chunk number. Usually 0 or 1. |
transactions | string | No | "rename" | How chunker should handle temporary files during transactions. One of rename, norename, auto. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/chunker" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "remote": "s3remote:large-files", "description": "Split large uploads into chunks", "chunk_size": "5368709120", "hash_type": "sha1", "transactions": "auto" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectChunker({ remote: "s3remote:large-files", description: "Split large uploads into chunks", chunk_size: "5368709120", hash_type: "sha1", transactions: "auto"});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_d6e7f8091a2b3c4d5e6f70819293a4b5", "type": "chunker", "backend_type": "chunker", "mount_paths": [] }}{ "success": false, "error": "remote must be specified"}POST /api/v1/backends/combine
Section titled “POST /api/v1/backends/combine”Combine several remotes into one logical namespace, with each upstream mounted under a subdirectory.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
upstreams | string | Yes | — | Upstreams for combining, in the form dir=remote:path dir2=remote2:path. Embedded spaces can be added using quotes, e.g. "dir=remote:path with space". |
description | string | No | "" | Description of the remote. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/combine" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "upstreams": "photos=s3remote:photos videos=s3remote:videos documents=gdrive:Documents", "description": "Aggregate photos, videos and documents" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectCombine({ upstreams: "photos=s3remote:photos videos=s3remote:videos documents=gdrive:Documents", description: "Aggregate photos, videos and documents"});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_e7f8091a2b3c4d5e6f70819293a4b5c6", "type": "combine", "backend_type": "combine", "mount_paths": [] }}{ "success": false, "error": "upstreams must be specified as 'dir=remote:path dir2=remote2:path'"}POST /api/v1/backends/hasher
Section titled “POST /api/v1/backends/hasher”Compute and cache checksums for an existing remote that does not expose them natively.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
remote | string | Yes | "" | Remote to cache checksums for (e.g. myRemote:path). |
description | string | No | "" | Description of the remote. |
auto_size | string | No | "0" | Auto-update checksum for files smaller than this size (disabled by default). |
hashes | string | No | "md5,sha1" | Comma separated list of supported checksum types. |
max_age | integer | No | 0 | Maximum time (in seconds) to keep checksums in cache. 0 = no cache, -1 = cache forever. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/hasher" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "remote": "s3remote:backups", "description": "Checksum cache for backups", "hashes": "md5,sha1,sha256", "max_age": 86400 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectHasher({ remote: "s3remote:backups", description: "Checksum cache for backups", hashes: "md5,sha1,sha256", max_age: 86400});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_f8091a2b3c4d5e6f70819293a4b5c6d7", "type": "hasher", "backend_type": "hasher", "mount_paths": [] }}{ "success": false, "error": "remote must be specified"}POST /api/v1/backends/local
Section titled “POST /api/v1/backends/local”Expose a local filesystem path with full rclone local semantics.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”All fields are optional.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | No | "" | Description of the remote. |
case_insensitive | boolean | No | false | Force the filesystem to report itself as case insensitive. |
case_sensitive | boolean | No | false | Force the filesystem to report itself as case sensitive. |
copy_links | boolean | No | false | Follow symlinks and copy the pointed to item. |
encoding | string | No | "33554434" | The encoding for the backend. |
links | boolean | No | false | Translate symlinks to/from regular files with a .hoody-vfslink extension. |
no_check_updated | boolean | No | false | Don’t check to see if the files change during upload. |
no_clone | boolean | No | false | Disable reflink cloning for server-side copies. |
no_preallocate | boolean | No | false | Disable preallocation of disk space for transferred files. |
no_set_modtime | boolean | No | false | Disable setting modtime after copying a file. |
no_sparse | boolean | No | false | Disable sparse files for multi-thread downloads. |
nounc | boolean | No | false | Disable UNC (long path names) conversion on Windows. |
one_file_system | boolean | No | false | Don’t cross filesystem boundaries (unix/macOS only). |
skip_links | boolean | No | false | Don’t warn about skipped symlinks. |
time_type | string | No | "0" | Set what kind of time is returned. One of mtime, atime, btime, ctime. |
unicode_normalization | boolean | No | false | Apply unicode NFC normalization to paths and filenames. |
zero_size_links | boolean | No | false | Assume the Stat size of links is zero (deprecated). |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/local" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "description": "Mount local scratch directory", "no_preallocate": true, "time_type": "mtime" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectLocal({ description: "Mount local scratch directory", no_preallocate: true, time_type: "mtime"});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_091a2b3c4d5e6f70819293a4b5c6d7e8", "type": "local", "backend_type": "local", "mount_paths": [] }}{ "success": false, "error": "Invalid time_type: must be one of mtime, atime, btime, ctime"}POST /api/v1/backends/memory
Section titled “POST /api/v1/backends/memory”In-memory object storage. Data is volatile and lost when the container restarts.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | No | "" | Description of the remote. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/memory" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "description": "Ephemeral scratch space" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectMemory({ description: "Ephemeral scratch space"});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_91a2b3c4d5e6f70819293a4b5c6d7e8f9", "type": "memory", "backend_type": "memory", "mount_paths": [] }}{ "success": false, "error": "Failed to initialize memory backend"}POST /api/v1/backends/union
Section titled “POST /api/v1/backends/union”Merge the contents of several upstreams into a single view, with configurable policies for reads, writes, and searches.
This endpoint takes no path, query, or header parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Default | Description |
|---|---|---|---|---|
upstreams | string | Yes | "" | List of space separated upstreams, e.g. "upstreama:test/dir upstreamb:" or "\"upstreama:test/space:ro dir\" upstreamb:". |
description | string | No | "" | Description of the remote. |
action_policy | string | No | "epall" | Policy to choose upstream on ACTION category. |
cache_time | integer | No | 120 | Cache time (in seconds) of usage and free space. Only useful when a path preserving policy is used. |
create_policy | string | No | "epmfs" | Policy to choose upstream on CREATE category. |
min_free_space | string | No | "1073741824" | Minimum viable free space for lfs/eplfs policies. |
search_policy | string | No | "ff" | Policy to choose upstream on SEARCH category. |
curl -X POST "https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/backends/union" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "upstreams": "primary:s3remote:primary secondary:s3remote:secondary", "description": "Tiered union with primary preferred", "create_policy": "epmfs", "search_policy": "ff", "min_free_space": "5368709120" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.backends.connectUnion({ upstreams: "primary:s3remote:primary secondary:s3remote:secondary", description: "Tiered union with primary preferred", create_policy: "epmfs", search_policy: "ff", min_free_space: "5368709120"});{ "success": true, "message": "Backend connected successfully", "data": { "id": "b_a2b3c4d5e6f70819293a4b5c6d7e8f90a", "type": "union", "backend_type": "union", "mount_paths": [] }}{ "success": false, "error": "upstreams must be a non-empty list of remote paths"}