The source decides what is shared
The share creator sets:
- Which directory to share (source_path)
- Access mode (readonly or readwrite)
- Who can access (1-to-1 or project-wide)
- When it expires (optional)
- Enable/disable anytime
Share a directory from one container to others on the same server, and, on servers where cross-server sharing is enabled, to containers on a different physical server. That covers multi-service applications, team collaboration, and data exchange without duplicating files.
Storage shares API:
Create and manage shares:
Receive and mount shares:
Storage shares use a two-party system:
The source decides what is shared
The share creator sets:
The target decides whether to mount
The share receiver controls:
Key principle: the source controls what is shared; the target controls whether it is mounted.
Share one directory with a single target container:
# Create 1-to-1 container share (readonly)hoody storage shares create --container $SOURCE_ID \ --source-path "/hoody/storage/shared-assets" \ --target-container-id $TARGET_ID \ --mode readonly \ --description "Static assets for frontend container"const share = await client.api.storage.shares.create(SOURCE_CONTAINER_ID, { source_path: '/hoody/storage/shared-assets', target_container_id: TARGET_CONTAINER_ID, mode: 'readonly', description: 'Static assets for frontend container'});console.log(share.data.id); // Share ID for mountingcurl -X POST "https://api.hoody.com/api/v1/containers/$SOURCE_ID/storage/shares" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source_path": "/hoody/storage/shared-assets", "target_container_id": "'$TARGET_ID'", "mode": "readonly", "description": "Static assets for frontend container" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the 1-to-1 share described above as a single GET link.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/SOURCE_ID/storage/shares&method=POST&bearer_token=TOKEN&json={"source_path":"/hoody/storage/shared-assets","target_container_id":"TARGET_ID","mode":"readonly","description":"Static%20assets%20for%20frontend%20container"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Use when:
Share a directory with every container in a project:
# Create project-wide share: all containers in project can accesshoody storage shares create --container $SOURCE_ID \ --source-path "/hoody/storage/config" \ --target-project-id $PROJECT_ID \ --mode readonly \ --description "Shared configuration for all services"const share = await client.api.storage.shares.create(SOURCE_CONTAINER_ID, { source_path: '/hoody/storage/config', target_project_id: PROJECT_ID, mode: 'readonly', description: 'Shared configuration for all services'});// Every container in the project automatically mounts this sharecurl -X POST "https://api.hoody.com/api/v1/containers/$SOURCE_ID/storage/shares" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source_path": "/hoody/storage/config", "target_project_id": "'$PROJECT_ID'", "mode": "readonly", "description": "Shared configuration for all services" }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the project-wide share described above as a single GET link.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/SOURCE_ID/storage/shares&method=POST&bearer_token=TOKEN&json={"source_path":"/hoody/storage/config","target_project_id":"PROJECT_ID","mode":"readonly","description":"Shared%20configuration%20for%20all%20services"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Every container in the project mounts this share by default.
Use when:
{ "mode": "readonly"}Target containers can:
Suited to:
{ "mode": "readwrite"}Target containers can:
Changes visible to:
Suited to:
Not suited to SQLite database files: see the caution under Cross-server sharing.
Across servers: read-write works where cross-server sharing is enabled. While it is disabled, the share is still created and returns success without mounting. See Cross-server sharing.
# In source containermkdir -p /hoody/storage/team-assets
# Add filescp logo.png /hoody/storage/team-assets/cp styles.css /hoody/storage/team-assets/ Response (201) returns the full share object, including id (the new share’s ID) needed for mounting.
Share creation already mounted this on its target(s); this step only changes that preference — send {"mount": false} to reject and unmount it, or {"mount": true} to remount it. Once mounted, the files are accessible in /hoody/shares/{share_alias}/.
# In target containerls /hoody/shares/shared-assets/# logo.png styles.css
# Files from source container, accessible in targetcat /hoody/shares/shared-assets/styles.cssWhen a target container mounts a share, the files appear at:
/hoody/shares/{share-alias}/Example:
# Source creates share with alias "config"POST /storage/shares{ "source_path": "/hoody/storage/app-config", "alias": "config"}
# Target mounts sharePATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}
# Files now accessible at:/hoody/shares/config/├── app.yaml├── database.json└── secrets.envThe alias is reflected in the mount point name. Hoody’s infrastructure determines the exact mount path, and the caller cannot set it directly.
A backend shares its upload directory with several frontends:
# Backend Container (source)POST /containers/{backend_id}/storage/shares{ "source_path": "/hoody/storage/user-uploads", "target_project_id": "{project_id}", "mode": "readwrite", "alias": "uploads"}
# Frontend Container 1 (accepts)PATCH /containers/{frontend_1}/storage/incoming/{share_id}/mount{"mount": true}
# Frontend Container 2 (accepts)PATCH /containers/{frontend_2}/storage/incoming/{share_id}/mount{"mount": true}
# Now all three containers see same /hoody/shares/uploads/# Upload from any frontend → visible to backend and other frontendsA config container shares settings with all services in readonly mode:
# Config ContainerPOST /containers/{config_id}/storage/shares{ "source_path": "/hoody/storage/production-config", "target_project_id": "{project_id}", "mode": "readonly", "alias": "config"}
# All service containers mount it# Services read from /hoody/shares/config/# Only config container can update (others readonly)Two developers share a workspace between their containers:
# Developer A's containerPOST /containers/{dev_a}/storage/shares{ "source_path": "/home/user/project", "target_container_id": "{dev_b_container}", "mode": "readwrite", "alias": "shared-project"}
# Developer B mounts in their container# Both containers read and write the same files# A write from one is visible to the otherServices share their logs with a monitoring container in readonly mode:
# Service 1POST /storage/shares{ "source_path": "/hoody/storage/service1/logs", "target_container_id": "{monitor_container}", "mode": "readonly"}
# Service 2POST /storage/shares{ "source_path": "/hoody/storage/service2/logs", "target_container_id": "{monitor_container}", "mode": "readonly"}
# Monitor container sees all logs/hoody/shares/service1-logs//hoody/shares/service2-logs/A share reports one of these statuses:
| Status | Description | Next Steps |
|---|---|---|
active | Successfully synced to the server | — |
failed | Mount failed (see status_message) | Check errors, fix, retry |
active means the share was successfully synced to the server. failed means a sync or mount error; read status_message for the error details.
Check status:
GET /api/v1/containers/{id}/storage/shares/{shareId}
# Response includes: "status": "active" AND "status_message"# status_message carries error details when status is failed.The source container can disable a share without deleting it:
# Disable share temporarilyPATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{ "enabled": false, "description": "Temporarily disabled for maintenance"}
# Files unmount from target containers# Share configuration preserved
# Re-enable laterPATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{ "enabled": true}Use for: maintenance windows, testing, and gradual rollouts.
Set an expiration timestamp at creation:
POST /api/v1/containers/{id}/storage/shares{ "source_path": "/hoody/storage/temp-files", "target_container_id": "{target}", "mode": "readonly", "expires_at": 1735689600 # Unix timestamp: 2025-01-01}
# At expires_at the share is auto-deleted, which unmounts it from every targetSuited to: temporary access, demo environments, and time-limited shares.
# Shares you created from specific containerhoody storage containers shares list --container $SOURCE_ID
# All shares you created (across all containers)hoody storage shares list// Shares from specific containerconst shares = await client.api.storage.shares.listByContainer(SOURCE_CONTAINER_ID);console.log(shares.data); // Array of shares you created
// All shares across all containersconst allShares = await client.api.storage.shares.listAll();# Shares you created from specific containercurl "https://api.hoody.com/api/v1/containers/$SOURCE_ID/storage/shares" \ -H "Authorization: Bearer $TOKEN"
# All shares you created (across all containers)curl "https://api.hoody.com/api/v1/storage/shares" \ -H "Authorization: Bearer $TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists the shares you created, either from one source container or across all of them.
# From one container
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/SOURCE_ID/storage/shares&method=GET&bearer_token=TOKEN&response=transparent
# All shares you created
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/storage/shares&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
# Incoming shares for specific containerhoody storage containers incoming list --container $TARGET_ID
# All incoming shares (all your containers)hoody storage incoming list// Incoming shares for specific containerconst incoming = await client.api.storage.shares.listIncomingByContainer(TARGET_CONTAINER_ID);console.log(incoming.data); // Shares offered to this container
// All incoming shares across all containersconst allIncoming = await client.api.storage.shares.listIncomingAll();# Incoming shares for specific containercurl "https://api.hoody.com/api/v1/containers/$TARGET_ID/storage/incoming" \ -H "Authorization: Bearer $TOKEN"
# All incoming shares (all your containers)curl "https://api.hoody.com/api/v1/storage/incoming" \ -H "Authorization: Bearer $TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists shares offered to you, either for one target container or across all of them.
# For one container
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/TARGET_ID/storage/incoming&method=GET&bearer_token=TOKEN&response=transparent
# All incoming shares
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/storage/incoming&method=GET&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
# Upgrade share from readonly to readwritehoody storage shares update --container $SOURCE_ID --share-id $SHARE_ID \ --mode readwrite --description "Now allows writes"await client.api.storage.shares.update(SOURCE_CONTAINER_ID, SHARE_ID, { mode: 'readwrite', description: 'Now allows writes' });curl -X PATCH "https://api.hoody.com/api/v1/containers/$SOURCE_ID/storage/shares/$SHARE_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode": "readwrite", "description": "Now allows writes"}'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Upgrades the share to readwrite as a single GET link.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/SOURCE_ID/storage/shares/SHARE_ID&method=PATCH&bearer_token=TOKEN&json={"mode":"readwrite","description":"Now%20allows%20writes"}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Target containers must remount to pick up the new mode.
# Delete share: unmounts from all target containershoody storage shares delete $SHARE_ID --yesawait client.api.storage.shares.delete(SHARE_ID);// Unmounts from all target containers, configuration deleted permanentlycurl -X DELETE "https://api.hoody.com/api/v1/storage/shares/$SHARE_ID" \ -H "Authorization: Bearer $TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Deletes the share as a single GET link, unmounting it from every target container.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/storage/shares/SHARE_ID&method=DELETE&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Cross-server sharing is a pilot feature and it is off by default. Where it is on, both read-only and read-write shares are permitted across hosts. Same-server shares are plain bind mounts and always work.
Once enabled, storage shares work across different physical servers with full POSIX compliance:
┌──────────────────────────────┐│ Server US-West-1 ││ ┌──────────────────────┐ ││ │ Source Container │ ││ │ /shared/data/ │ ││ └──────────────────────┘ ││ ↓ Hoody handles │└──────────────────────────────┘ ↓ cross-server┌──────────────────────────────┐│ Server EU-Central-1 ││ ┌──────────────────────┐ ││ │ Target Container │ ││ │ /hoody/shares/data/ │ ││ └──────────────────────┘ │└──────────────────────────────┘What you get once it is enabled:
The share remains mounted but inaccessible.
/hoody/shares/{alias}/Best practice: do not rely on shares from containers that stop frequently.
Yes. Create multiple shares from the same source_path:
# Share /hoody/storage/assets with 3 containersPOST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "A"}POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "B"}POST /storage/shares {"source_path": "/hoody/storage/assets", "target_container_id": "C"}
# Or share once to entire project (all containers see it)POST /storage/shares {"source_path": "/hoody/storage/assets", "target_project_id": "{project}"}You can, and you should not put SQLite database files in the shared directory:
# Do not do this with a database insidePOST /storage/shares{ "source_path": "/hoody/databases", "mode": "readwrite"}Why: SQLite’s safety rests on the filesystem honoring its locks. A share is a mount, and a cross-server one is NFS, where that is not guaranteed. Two containers opening the same database file through a share risk corruption, not just lock timeouts.
Better solutions:
Share remains available but unmounted. Target can accept later:
# Initially rejectPATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": false}
# Accept later when neededPATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}
# Files appear in /hoody/shares/{alias}/Yes. Share configuration and mount state survive:
Yes:
PATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{ "alias": "new-alias"}Target containers must unmount and remount to use the new alias. The old mount point /hoody/shares/old-alias/ becomes invalid.
Source container: the files count toward the source’s storage.
Target containers: mounted shares do not count toward the target’s storage quota, because they are references rather than copies.
Problem: Share status is "failed" with error message
Common causes:
Source path doesn’t exist:
# In source container, verify path existsls -la /hoody/storage/shared-path
# Create if missingmkdir -p /hoody/storage/shared-path
# Update share to retryPATCH /api/v1/containers/{source_id}/storage/shares/{share_id}{"enabled": true}Permission issues:
# Fix permissions in source containerchown -R root:root /hoody/storage/shared-pathchmod -R 755 /hoody/storage/shared-pathSource container stopped:
Problem: Share mounted but /hoody/shares/{alias}/ is empty
Solutions:
Verify the share is active, and read status_message:
GET /api/v1/containers/{target_id}/storage/incoming# Check: "status": "active", "enabled": true# status_message carries error details for failed shares.# This response does not report whether this consumer is mounted.Check source container is running:
GET /api/v1/containers/{source_id}# Verify: "status": "running"Verify files exist in source:
# In source containerls /hoody/storage/shared-path# Should show filesUnmount and remount:
PATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": false}
PATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}Problem: Cannot access files in /hoody/shares/{alias}/
Cause: Source container restarted while target was accessing files
Solution:
# Unmount and remountPATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": false}
PATCH /api/v1/containers/{target_id}/storage/incoming/{share_id}/mount{"mount": true}
# Or restart target container (auto-remounts)POST /api/v1/containers/{target_id}/restart# (Consolidated lifecycle route: POST /api/v1/containers/{id}/{operation}# where {operation} is one of: start | stop | force-stop | restart | pause | resume)Problem: PATCH /mount succeeds but files don’t appear
Check:
Share is enabled:
GET /api/v1/containers/{target_id}/storage/incoming# Find the share in the list and verify: "enabled": trueShare not expired:
# Check expires_at (Unix timestamp)# If expired, the share is already deleted and gone from this list — create a new oneTarget container has permission:
# Good: Specific subdirectory{"source_path": "/hoody/storage/assets"}
# Risky: Entire Hoody Kit storage{"source_path": "/hoody/storage"}
# Sharing entire /hoody/storage exposes all service dataOne share for all containers:
# Instead of creating 10 identical 1-to-1 sharesPOST /storage/shares {"target_project_id": "{project}"}
# All containers in project can mount# Simpler management, one configurationMultiple writers can conflict:
# Container A writes /hoody/shares/data/file.txt# Container B writes /hoody/shares/data/file.txt (same file)
# Last write wins (potential data loss)Solution: use application-level locking, or coordinate writes (for example, a different directory per container).
For SQLite databases, do not share the file at all. Keep it in one container and query it over the hoody-sqlite HTTP API.
Deleting a share unmounts it from every target:
# Snapshot source container firstPOST /api/v1/containers/{source_id}/snapshots{"alias": "before-share-deletion"}
# Then delete shareDELETE /api/v1/storage/shares/{share_id}# Clear documentation{ "description": "Read-only access to team logo, CSS, and JS assets for frontend containers. Source: /hoody/storage/static-web-assets"}
# Vague{ "description": "shared files"}This matters once you are managing dozens of shares.
Storage ecosystem:
Related features:
Summary:
/hoody/databases/ and share them through the hoody-sqlite HTTP API