SQLite
Serverless databases over HTTP: queries, a KV store, and time-travel reads.
Every file is a URL. Read, download, hash, and browse files across local storage and 60+ cloud providers (Google Drive, Dropbox, S3, OneDrive, and others) through one HTTP interface.
Every Hoody container runs hoody-files, which provides that access wherever the files live.
hoody-files exposes storage as HTTP endpoints:
?zip parameterEach link below goes to the reference page for that endpoint, with all parameters, responses, and examples.
File reading and downloading:
backend, base64json, simple, content-typeFile integrity:
Archive operations:
Directory listing:
sort (name|size|mtime), order (asc|desc)Backend management:
System monitoring:
Open the container files URL in a browser:
https://{project}-{container}-files-1.{server}.containers.hoody.comThe interface provides:
Use it for daily file management, quick edits, browsing cloud storage, reviewing logs, and editing config files, none of which require leaving the browser. The interface is the same on a phone, a tablet, and a laptop.
The same request shape works against every mounted backend:
# Container files URL: https://{project}-{container}-files-1.{server}.containers.hoody.com
# Local container filescurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/report.pdf"
# Google Drivecurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/Work/report.pdf?backend=8f3a2c1e4b5d6f7a"
# Amazon S3curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/backups/data.zip?backend=2b9d4e6a1c3f5078"
# Dropboxcurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/Photos/vacation.jpg?backend=7c1e9a3b5d2f4860"Only the backend parameter differs between them.
Connect a storage provider once:
hoody files backends connect drive \ --client-id "your-app.apps.googleusercontent.com" \ --client-secret "your-secret" \ --token '{"access_token":"ya29..."}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
const backend = await containerClient.files.backends.connectDrive({ client_id: 'your-app.apps.googleusercontent.com', client_secret: 'your-secret', token: '{"access_token":"ya29..."}',});console.log(backend.data.id); // 16-char hex backend IDcurl -X POST "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/backends/drive" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-app.apps.googleusercontent.com", "client_secret": "your-secret", "token": "{\"access_token\":\"ya29...\"}" }'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
Connects a Google Drive backend to this container in one request. The client secret and token travel inside the link itself, so treat it like the raw credential rather than pasting it somewhere shared.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/backends/drive&method=POST&json={"client_id":"your-app.apps.googleusercontent.com","client_secret":"your-secret","token":"{\"access_token\":\"ya29...\"}"}&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.
Response (201 Created). The echoed config masks values whose key matches a known-sensitive name (password, token, secret, client_secret, private_key and similar). It is a name list, not content inspection, so a credential stored under an unusual key comes back in the clear:
{ "success": true, "message": "drive backend connected successfully", "data": { "id": "9b4d3e2f5a6c7890", "type": "drive", "vfs_backend_type": "drive", "config": { "client_id": "your-app.apps.googleusercontent.com", "client_secret": "***", "token": "***" }, "mount_paths": [] }}The returned backend ID then addresses every Drive file:
# List Drive rootcurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/?backend=8f3a2c1e4b5d6f7a"
# Download Drive filecurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=8f3a2c1e4b5d6f7a" \ --output report.pdfThe mount persists across container restarts, so you connect a provider once.
Pick the format that suits the caller:
# Interactive file browseropen "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/"Interactive file browser with visual navigation, search, sortable columns, and upload interface (if permitted).
# Structured datacurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/?json"{ "kind": "Index", "paths": [ { "name": "report.pdf", "path_type": "File", "size": 524288, "mtime": 1699564800000 } ]}# Clean text for scriptscurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/images/?simple"photo1.jpgphoto2.jpgvacation/One item per line. Directories end with /.
Check a download against the hash the service reports:
# Container files URLFILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
# 1. Get expected hashexpected=$(curl -s "$FILES_URL/api/v1/files/large-file.bin?hash")
# 2. Download filecurl "$FILES_URL/api/v1/files/large-file.bin" -o large-file.bin
# 3. Verifyactual=$(sha256sum large-file.bin | awk '{print $1}')
if [ "$expected" = "$actual" ]; then echo "✓ Download verified"else echo "✗ Download corrupted"fiThis matters most for:
Preview an archive without extracting it:
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/backup.tar.gz?preview"# Lists contents without downloading full archiveBase64-encode a file for embedding:
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/config.json?base64"# Returns: eyJrZXkiOiJ2YWx1ZSJ9Override the content type:
curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/data.txt?content-type=text/csv"# Forces download as CSVDownload a whole directory as a zip:
# Download entire directory as .zip archivecurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/?zip" \ -o documents.zipThe file service runs as root inside the container, but the files and folders it
creates are owned by your container user (user), not root. The interactive
shell and your processes can therefore read, edit, and delete everything the file manager makes.
This applies to every operation that creates a new inode: uploads, new folders
(?mkdir), touch, appends to a new file, archive extraction (every extracted entry),
copies, downloads (?download_from), and any parent directories created along the way.
Existing files keep their owner: overwriting, appending to, or moving an existing
file never changes who owns it.
Operator configuration (CLI flags / env on the file service):
| Flag | Env | Default | Meaning |
|---|---|---|---|
--default-create-owner <spec> | HOODY_FILE_MANAGER_DEFAULT_CREATE_OWNER | user | Owner for newly-created inodes. spec is user, user:group, uid, uid:gid, or none/off to disable (inherit the process owner = root). |
--allowed-create-owners <csv> | HOODY_FILE_MANAGER_ALLOWED_CREATE_OWNERS | (empty) | Owners a client owner= override may request (the default owner is always allowed). Resolved per request. |
Ownership is fail-closed: when the feature is active the service verifies it can
change ownership at startup, and if a per-operation ownership change unexpectedly fails
the request returns 500 and the partially-created file/dir is rolled back: a created
file is never silently left owned by root.
The journal records every mutation. Each time a file is created, written, appended, deleted, moved, copied, or has its permissions changed, the journal captures the event along with a content-addressable blob snapshot of the file at that moment. You can therefore read any file as it existed at any past revision, at any point in time, or compute diffs between any two versions.
The journal is best-effort: file operations always succeed even if journaling encounters an error. Under normal conditions every mutation is captured, which gives you a complete audit trail of your container’s filesystem.
See every revision of a file:
hoody files get src/app.ts --history --limit 50import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });const { data } = await client.api.containers.list();const containerClient = await client.withContainer(data.containers![0]!);
const history = await containerClient.files.get('src/app.ts', { history: '', limit: 50 });
for (const rev of history.data.revisions) { console.log(`#${rev.seq} [${rev.op}] ${rev.ts}`);}curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/src/app.ts?history&limit=50"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 recorded revisions of src/app.ts, 50 per page.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/src/app.ts?history%26limit=50&method=GET&response=transparent Retrieve the exact content of a file at any point in its history:
# By revision numberhoody files get src/app.ts --revision 3
# By timestamphoody files get src/app.ts --at "2026-03-19T14:30:00Z"// By revisionconst v3 = await containerClient.files.get('src/app.ts', { revision: 3 });
// By timestampconst yesterday = await containerClient.files.get('src/app.ts', { at: '2026-03-19T14:30:00Z' });# By revisioncurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/src/app.ts?revision=3"
# By timestampcurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/src/app.ts?at=2026-03-19T14:30:00Z"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
Fetches src/app.ts exactly as it was at a given revision number or timestamp.
# By revision
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/src/app.ts?revision=3&method=GET&response=transparent
# By timestamp
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/src/app.ts?at=2026-03-19T14:30:00Z&method=GET&response=transparent Compute a unified diff between any two versions of a file:
hoody files get src/app.ts --diff --from-seq 1 --to-seq 3const diff = await containerClient.files.get('src/app.ts', { diff: '', from_seq: 1, to_seq: 3 });console.log(diff);curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/src/app.ts?diff&from_seq=1&to_seq=3"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
Returns a unified diff of src/app.ts between revision 1 and revision 3.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/src/app.ts?diff%26from_seq=1%26to_seq=3&method=GET&response=transparent Search across all mutations in your container and monitor journal health:
# Query recent writes under src/hoody files query --path src/ --op write --limit 20
# View journal storage statisticshoody files stats// Query journal entriesconst entries = await containerClient.files.journal.query({ path: 'src/', op: 'write', limit: 20 });
// Get journal statsconst stats = await containerClient.files.journal.getStats();console.log(`${stats.data.total_entries} entries, ${stats.data.total_blobs} blobs`);# Query journalcurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/journal?path=src/&op=write&limit=20"
# Journal statscurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/journal/stats"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
Searches recent write events under src/ and reports overall journal storage stats.
# Query journal
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/journal?path=src/%26op=write%26limit=20&method=GET&response=transparent
# Journal stats
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/journal/stats&method=GET&response=transparent Different tools for different storage:- AWS CLI for S3- Google Drive SDK- Dropbox API- SFTP client- WebDAV client
Each with different authentication, different SDKs, different patterns.That means:
One HTTP API for everything:https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/{path}?backend={provider}That gives you:
A phone browser reaches the same files:
// From mobile browserawait fetch('https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/files/documents/contract.pdf?backend=8f3a2c1e4b5d6f7a') .then(r => r.blob()) .then(blob => { // View PDF on phone });The device needs no Google Drive app, no Dropbox app, and no S3 client, only the ability to make an HTTP request.
Read files from several providers in one pass:
// List all mounted backendsconst filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
const backends = await fetch(filesUrl + '/api/v1/backends').then(r => r.json());
// Access files from eachfor (const backend of backends.backends) { const files = await fetch(filesUrl + `/api/v1/files/?backend=${backend.id}`) .then(r => r.json());
console.log(`${backend.backend_type}: ${files.paths.length} files`);}Download a file and check it against the reported hash:
async function verifiedDownload(path, backend) { // 1. Get expected hash const hashResponse = await fetch( `/api/v1/files${path}?backend=${backend}&hash` ); const expectedHash = await hashResponse.text();
// 2. Download file const fileResponse = await fetch( `/api/v1/files${path}?backend=${backend}` ); const blob = await fileResponse.blob();
// 3. Verify hash (browser) const arrayBuffer = await blob.arrayBuffer(); const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer); const actualHash = Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join('');
if (actualHash === expectedHash) { console.log('✓ Download verified'); return blob; } else { throw new Error('Download corrupted - hash mismatch'); }}Confirm that every local file exists in the cloud backup:
import requests
files_url = 'https://{project}-{container}-files-1.{server}.containers.hoody.com'
def verify_backup(local_path, backup_backend): # Get local listing local = requests.get(f'{files_url}{local_path}?json').json()
# Get backup listing backup = requests.get( f'{files_url}/api/v1/files{local_path}', params={'backend': backup_backend} ).json()
local_files = {p['name']: p['size'] for p in local['paths'] if p['path_type'] == 'File'} backup_files = {p['name']: p['size'] for p in backup['paths'] if p['path_type'] == 'File'}
missing = set(local_files.keys()) - set(backup_files.keys()) size_mismatch = [ name for name in local_files if name in backup_files and local_files[name] != backup_files[name] ]
if not missing and not size_mismatch: print(f'✓ Backup complete: {len(local_files)} files verified') else: print(f'✗ Missing {len(missing)} files, {len(size_mismatch)} size mismatches')An agent reads a file over HTTP and sends it to a model:
// AI agent reads code fileconst filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
const code = await fetch( filesUrl + '/api/v1/files/app/main.js?backend=8f3a2c1e4b5d6f7a').then(r => r.text());
// AI analyzesconst analysis = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: `Review this code:\n\n${code}` }]});
// AI can directly access your files from cloud storage// No downloading to local machine neededCompare the local and remote listings, then fetch what is missing:
#!/bin/bash
FILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
# Get local directorylocal=$(curl -s "$FILES_URL/documents/?json")
# Get remote directoryremote=$(curl -s "$FILES_URL/api/v1/files/documents/?backend=2b9d4e6a1c3f5078")
# Compare and download missing filesecho "$remote" | jq -r '.paths[] | select(.path_type == "File") | .name' | \while read filename; do if ! echo "$local" | jq -e ".paths[] | select(.name == \"$filename\")" > /dev/null; then echo "Downloading: $filename" curl "$FILES_URL/api/v1/files/documents/$filename?backend=2b9d4e6a1c3f5078" \ -o "documents/$filename" fidoneOne HTTP interface replaces the per-provider CLIs:
# Traditional: Different CLI for each provideraws s3 cp s3://bucket/file.pdf ./ # AWS CLIgcloud storage cp gs://bucket/file.pdf ./ # Google CLIaz storage blob download ... # Azure CLIrclone copy dropbox:file.pdf ./ # Rclone
# Hoody: One HTTP interfaceFILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
curl "$FILES_URL/api/v1/files/file.pdf?backend=2b9d4e6a1c3f5078" -o file.pdfcurl "$FILES_URL/api/v1/files/file.pdf?backend=5a7c9e1b3d2f4860" -o file.pdfcurl "$FILES_URL/api/v1/files/file.pdf?backend=6b8d0f2a4c1e3759" -o file.pdfcurl "$FILES_URL/api/v1/files/file.pdf?backend=7c1e9a3b5d2f4860" -o file.pdfA phone browser reaches every mounted backend:
// Phone browserconst filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
const file = await fetch( filesUrl + '/api/v1/files/documents/contract.pdf?backend=8f3a2c1e4b5d6f7a');const blob = await file.blob();
// View PDF directly in mobile browser// No app installation neededGoogle Drive, S3, and Dropbox are all reachable from the mobile browser, because every file is an HTTP resource.
Embed a file browser in a page:
<!-- Live file browser in documentation --><iframe src="https://demo-files.hoody.com/examples/?backend=3d5f7a9c1e2b4068&readonly=true" height="400" />Readers see the actual files rather than a description of them.
Verify backups over HTTP:
#!/bin/bash# Nightly backup verificationFILES_URL="https://{project}-{container}-files-1.{server}.containers.hoody.com"
for file in $(curl -s "$FILES_URL/critical/?simple"); do # Get hash from local local_hash=$(curl -s "$FILES_URL/critical/$file?hash")
# Get hash from S3 backup backup_hash=$(curl -s "$FILES_URL/api/v1/files/critical/$file?backend=2b9d4e6a1c3f5078&hash")
if [ "$local_hash" != "$backup_hash" ]; then echo "WARNING: Backup mismatch: $file" # Re-upload to S3 via other tools or trigger alert fidoneAn agent lists, reads, and categorizes files:
const filesUrl = 'https://{project}-{container}-files-1.{server}.containers.hoody.com';
// AI agent lists filesconst files = await fetch(filesUrl + '/api/v1/files/?backend=8f3a2c1e4b5d6f7a') .then(r => r.json());
// AI analyzes and categorizesfor (const file of files.paths) { if (file.path_type === 'File') { const content = await fetch( filesUrl + `/api/v1/files/${file.name}?backend=8f3a2c1e4b5d6f7a` ).then(r => r.text());
// AI determines category const category = await ai.categorize(content);
// AI can move files, create folders, organize automatically }}Move a file between cloud providers:
// Read from Google Driveconst file = await fetch( '/api/v1/files/document.pdf?backend=8f3a2c1e4b5d6f7a').then(r => r.blob());
// Upload to S3 (via other tools/APIs)// Or use hoody-files to bridge providersConnect every provider when the container is created:
const providers = [ { type: 'drive', credentials: googleCreds }, { type: 's3', credentials: awsCreds }, { type: 'dropbox', credentials: dropboxCreds }];
for (const provider of providers) { await fetch(`/api/v1/backends/${provider.type}`, { method: 'POST', body: JSON.stringify(provider.credentials) });}
// Now all storage accessible through one interfaceFor production data, system backups, or compliance checks:
hash=$(curl -s "$URL?hash")curl "$URL" -o fileecho "$hash file" | sha256sum -cEach format suits a different consumer:
Directory listings change infrequently:
const cache = new Map();
async function getDirectory(path, backend, ttl = 60000) { const key = `${path}:${backend}`; const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttl) { return cached.data; }
const data = await fetch(`/api/v1/files${path}?backend=${backend}`) .then(r => r.json());
cache.set(key, { data, time: Date.now() }); return data;}Check that a backend is healthy first:
# Test connectioncurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/backends/{backend_id}/test"
# Check if successful before bulk operationsCloud providers throttle API calls:
// Add delays between requestsfor (const file of files) { await downloadFile(file); await new Promise(r => setTimeout(r, 100)); // 100ms delay}
// Or use Cache backend to reduce provider API callsUnlimited. Mount Google Drive, S3, Dropbox, OneDrive, Box, and 50+ others all in one container. Each gets a unique backend ID. Access files from any provider through the same HTTP interface.
Yes. Backend configurations are stored in the container’s filesystem. After a restart, all previously mounted storage is reconnected automatically, so you do not re-authenticate each time.
Yes. Different Google Drive accounts, S3 buckets, or Dropbox folders each mount as a separate backend with its own ID, which is how you keep multiple tenants or client accounts apart.
Local files: sub-millisecond access. Cloud files: 50-500ms depending on provider and distance. Use Cache backend to speed up frequently accessed remote files. Or sync important files locally.
Yes. An agent makes standard HTTP requests to hoody-files endpoints, so listing directories, reading files, and verifying hashes all work over plain HTTP with no provider-specific SDK.
Yes, but limited to local container storage or specific backends that support writing. Most cloud providers require OAuth scopes for write access. Check backend documentation for write capabilities.
Use container proxy permissions to control who can access files. Configure IP whitelist, password auth, or JWT validation. Until you configure permissions, access is gated only by knowledge of the URL — anyone holding it can reach the files.
Yes. Open the hoody-files URL in your mobile browser: the HTML format gives you a visual file browser, and the JSON format works for a custom mobile app. Google Drive, S3, and local container files are all reachable from the phone through the same interface.
File operations return 401 Unauthorized. Rotate the credentials using PUT /api/v1/backends/{id} with the fresh token or secret, which preserves the existing backend ID. If you need to change identity fields (host, user, type), delete the backend and reconnect with POST /api/v1/backends/{type} to get a new ID.
Problem: Cannot mount storage provider
Solutions:
Verify credentials are correct:
# Check OAuth tokens haven't expired# Verify API keys are valid# Ensure client_id/client_secret matchCheck network connectivity:
hoody files backends test {backend_id}const test = await containerClient.files.backends.testConnection('{backend_id}');console.log(test.status); // "connected" or "disconnected"curl "https://{project}-{container}-files-1.{server}.containers.hoody.com/api/v1/backends/{backend_id}/test"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
Tests the connection for one mounted backend without running any file operations.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/backends/BACKEND_ID/test&method=GET&response=transparent Problem: File exists but getting 404 response
Check:
Path is case-sensitive:
# Correct: /documents/file.pdfVerify file exists:
# List parent directorycurl "https://{project}-{container}-files-1.{server}.containers.hoody.com/documents/?json"Backend ID is correct:
# List all backendsGET /api/v1/backends# Use correct backend_idProblem: Downloaded file hash doesn’t match expected
Possible causes:
Solution:
# Use curl resume supportcurl -C - "$URL" -o file
# Verify againecho "$expected_hash file" | sha256sum -cProblem: Cloud provider returning too many requests error
Solutions:
Add delays between requests:
for (const file of files) { await fetch(fileUrl); await new Promise(r => setTimeout(r, 200)); // 200ms delay}Use Cache backend:
# Mount cache in front of providerPOST /api/v1/backends/cache{ "remote": "s3_backend:bucket", "chunk_size": "10M"}Batch operations when possible - Download multiple files in one session
Problem: Timeout or incomplete download for large files
Solutions:
Use curl with resume support:
curl -C - "https://{project}-{container}-files-1.{server}.containers.hoody.com/large-file.bin" -o large-file.binCheck disk space before downloading:
size=$(curl -sI "$URL" | grep Content-Length | awk '{print $2}')available=$(df -P . | tail -1 | awk '{print $4}')# Ensure available > size before downloadingDownload in chunks if supported:
# Some backends support Range requestscurl -r 0-104857600 "$URL" > part1 # First 100MBcurl -r 104857601- "$URL" > part2 # Restcat part1 part2 > complete-fileThe other data services in the kit:
SQLite
Serverless databases over HTTP: queries, a KV store, and time-travel reads.
Exec
Scripts become HTTP endpoints, so your code is served as an API.
cURL
Complex HTTP operations reduced to a single GET request against any REST API.
More on file operations: