Files
Access files across local storage and 60+ cloud providers through HTTP.
Your database is a URL. Execute SQL via HTTP, store key-value data, travel through time, perform atomic operations—all serverless, all through HTTP endpoints.
Every Hoody container includes hoody-sqlite, providing serverless database access with both traditional SQL and a powerful Key-Value store interface.
hoody-sqlite has two modes for the ?db= parameter:
?db=notes resolves to ./notes.db relative to the server’s working directory. Absolute paths like ?db=/data/app.db are rejected with INVALID_DB_PATH.--allow-any-absolute-db-path when starting the binary. Absolute paths (/data/app.db) are then accepted and used verbatim.In Hoody containers this flag is ON by default, which is why examples throughout this page use ?db=/data/*.db. If you’re running the binary yourself, either drop the absolute path or add the flag.
Stacks perfectly with SQLite Drive →: Store databases in /hoody/databases/ for multi-container access (concurrent write-safe). Query via HTTP through hoody-sqlite OR directly with traditional SQLite libraries (Python sqlite3, Node better-sqlite3, etc.)—mix both approaches freely.
hoody-sqlite provides dual database interfaces through HTTP:
Official Technical Reference:
For complete endpoint documentation with all parameters, responses, and examples:
SQL Operations:
sql (Base64-encoded SELECT statement)path, init_kv (auto-create KV table)Key-Value Store - Basic:
db, table, path (JSON path), at_timestamp (time-travel, Unix timestamp integer)ttl (auto-expiry), if_match (CAS), path (partial update), historyKey-Value Store - Batch:
Key-Value Store - Atomic:
delta (amount to add, default: 1)delta (amount to subtract, default: 1){"value": item} (add to end of array){"value": item} or query param index (position)Key-Value Store - Time-Travel:
limit (pagination)op_number (integer, required — get from /history response)from, to (Unix timestamps)steps (number of changes to undo, default: 1)timestamp (Unix timestamp integer)to_timestamp (Unix timestamp integer, required), confirm=yes (required to apply — omit for dry-run preview)Query History:
Web Interface:
System Monitoring:
Open the container SQLite URL in your browser for visual database management:
https://{project}-{container}-sqlite-1.{server}.containers.hoody.icuInteractive web interface with:
Perfect for: Exploring databases, testing queries, viewing table contents, debugging data issues—all without leaving the browser.
Works on any device: Phone, tablet, laptop—same interface everywhere.
No database server needed. Just HTTP requests:
# Execute a SQL transactionhoody db exec-transaction --transaction '{ "transaction": [ { "statement": "INSERT INTO users (name, email) VALUES (?, ?)", "values": ["Alice", "alice@example.com"] }, { "query": "SELECT * FROM users WHERE email = ?", "values": ["alice@example.com"] } ]}'import { HoodyClient } from '@hoody-ai/hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER });
const result = await containerClient.sqlite.database.executeTransaction({ transaction: [ { statement: 'INSERT INTO users (name, email) VALUES (?, ?)', values: ['Alice', 'alice@example.com'] }, { query: 'SELECT * FROM users WHERE email = ?', values: ['alice@example.com'] }, ],});console.log(result.data); // { results: [{ rowsUpdated: 1 }, { resultSet: [{ id: 1, name: "Alice", ... }] }] }curl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/db?db=/data/app.db" \ -H "Content-Type: application/json" \ -d '{ "transaction": [ { "statement": "INSERT INTO users (name, email) VALUES (?, ?)", "values": ["Alice", "alice@example.com"] }, { "query": "SELECT * FROM users WHERE email = ?", "values": ["alice@example.com"] } ] }' Response includes query results:
{ "results": [ {"rowsUpdated": 1}, {"resultSet": [{"id": 1, "name": "Alice", "email": "alice@example.com"}]} ]}The breakthrough: Your database is accessible via HTTP from:
NoSQL-style operations on SQLite:
# Set a key-value pairhoody kv set "user:1" --body '{"name": "Alice", "role": "editor"}'
# Get value by keyhoody kv get "user:1"
# Delete a keyhoody kv delete "user:1"
# Atomic incrementhoody kv incr "views:homepage"// Set a key-value pairawait containerClient.sqlite.kvStore.set('user:1', JSON.stringify({ name: 'Alice', role: 'editor' }));
// Get value by keyconst user = await containerClient.sqlite.kvStore.get('user:1');console.log(user); // { name: "Alice", role: "editor" }
// Delete a keyawait containerClient.sqlite.kvStore.delete('user:1');
// Atomic increment (thread-safe)await containerClient.sqlite.kvStore.incr('views:homepage');# Set a key-value paircurl -X PUT "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/data/app.db" \ -H "Content-Type: application/json" \ -d '{"name": "Alice", "role": "editor"}'
# Get value by keycurl "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/data/app.db"
# Delete a keycurl -X DELETE "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/user:1?db=/data/app.db"
# Atomic incrementcurl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.icu/api/v1/sqlite/kv/views:homepage/incr?db=/data/stats.db" Get the value back:
Perfect for:
Thread-safe operations for shared state:
Multiple clients calling simultaneously? No problem - each increment is atomic. Counter goes: 5 -> 6 -> 7 -> 8 (not 5 -> 6 -> 6 -> 6)
Atomic operations:
# Increment counterPOST /api/v1/sqlite/kv/{key}/incr?delta=1
# Decrement inventoryPOST /api/v1/sqlite/kv/inventory:item1/decr?delta=1
# Perfect for: views, likes, credits, inventory# Add item to shopping cartPOST /api/v1/sqlite/kv/cart:user1/push{"value": "product-123"}
# Remove last itemPOST /api/v1/sqlite/kv/cart:user1/pop
# Remove specific itemPOST /api/v1/sqlite/kv/cart:user1/remove{"value": "product-123"}No race conditions. No locks needed. Just atomic HTTP operations.
Operate on 100 keys in one atomic request:
// Set multiple keys atomicallyawait fetch('.../kv/batch/set?db=/data/app.db', { method: 'POST', body: JSON.stringify({ batch: [ { key: 'user:1', value: { name: 'Alice' } }, { key: 'user:2', value: { name: 'Bob' } }, { key: 'user:3', value: { name: 'Carol' } } ] })});
// Get multiple keys in one requestawait fetch('.../kv/batch/get?db=/data/app.db', { method: 'POST', body: JSON.stringify({ keys: ['user:1', 'user:2', 'user:3'] })});100x faster than individual requests. All operations succeed or all fail (atomic).
Every change is recorded:
# View complete change history (returns op_number for each entry)GET /api/v1/sqlite/kv/config:timeout/history?db=/data/app.db
# Get value at a specific operation number (op_number from history)GET /api/v1/sqlite/kv/config:timeout/snapshot?op_number=42&db=/data/app.db
# Undo last changePOST /api/v1/sqlite/kv/config:timeout/rollback?db=/data/app.db
# Restore entire database to 2 hours ago (dry-run preview — add &confirm=yes to apply)timestamp=$(date -d '2 hours ago' +%s)POST /api/v1/sqlite/kv/rollback?to_timestamp=$timestamp&db=/data/app.dbPerfect for:
Create read-only query links:
// 1. Define queryconst sql = "SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC";
// 2. Encode as Base64const encoded = btoa(sql);
// 3. Create shareable URLconst queryUrl = `/api/v1/sqlite/query?db=/data/app.db&sql=${encoded}`;
// Share this URL - anyone can view live data// No write access, perfectly safeUse cases:
Database Server (install) → Connection Pool (configure) → Client Library (install) → Query (finally)Problems:
HTTP Request → SQLite File → Response (immediately)Advantages:
Because databases are HTTP:
Your phone can query databases:
// From mobile browserconst users = await fetch( 'https://{project}-{container}-sqlite-1.{server}.containers.hoody.icu/api/v1/sqlite/db?db=/data/app.db', { method: 'POST', body: JSON.stringify({ transaction: [{ query: 'SELECT * FROM users LIMIT 10' }] }) }).then(r => r.json());AI agents have native database access:
// AI makes standard HTTP requestawait fetch(sqliteUrl + '/api/v1/sqlite/db?db=/data/app.db', { method: 'POST', body: JSON.stringify({ transaction: [{ query: 'SELECT COUNT(*) FROM orders WHERE status = "pending"' }] })});// No database driver. No connection string. Just HTTP.Embed live data in pages:
<iframe src="https://{project}-{container}-sqlite-1.{server}.containers.hoody.icu/api/v1/sqlite/query?db=/data/stats.db&sql=U0VMRUNUIC4uLg==" />Store and retrieve config via KV store:
// Set configurationawait fetch('.../kv/config:api?db=/data/app.db', { method: 'PUT', body: JSON.stringify({ timeout: 30, retries: 3, endpoint: 'https://api.example.com' })});
// Get configurationconst config = await fetch('.../kv/config:api?db=/data/app.db') .then(r => r.json());
// Use in appconst response = await fetch(config.endpoint, { timeout: config.timeout });Track metrics across multiple clients:
// Each page view increments atomicallyawait fetch('.../kv/views:homepage/incr?db=/data/stats.db', { method: 'POST'});
// Get current countconst count = await fetch('.../kv/views:homepage?db=/data/stats.db') .then(r => r.json());
console.log(`Homepage views: ${count}`);No race conditions. 1000 simultaneous requests = counter increments exactly 1000.
Store user sessions with auto-expiry:
// Create session with 1-hour TTLawait fetch('.../kv/session:abc123?db=/data/sessions.db&ttl=3600', { method: 'PUT', body: JSON.stringify({ user_id: 'user-456', ip: '203.0.113.50', created_at: Date.now() })});
// Session automatically expires after 1 hour// No cleanup jobs neededAtomic cart operations:
// Add item to cartawait fetch('.../kv/cart:user1/push?db=/data/store.db', { method: 'POST', body: JSON.stringify({ value: { product_id: 'prod-xyz', quantity: 2, price: 29.99 } })});
// Remove itemawait fetch('.../kv/cart:user1/remove?db=/data/store.db', { method: 'POST', body: JSON.stringify({ value: { product_id: 'prod-xyz' } })});
// Get entire cartconst cart = await fetch('.../kv/cart:user1?db=/data/store.db') .then(r => r.json());Use both interfaces in one database:
// SQL for complex queriesconst analytics = await fetch('.../api/v1/sqlite/db?db=/data/app.db', { method: 'POST', body: JSON.stringify({ transaction: [{ query: 'SELECT product_id, COUNT(*) as purchases FROM orders GROUP BY product_id ORDER BY purchases DESC LIMIT 10' }] })}).then(r => r.json());
// KV for simple configconst config = await fetch('.../kv/config:featured?db=/data/app.db') .then(r => r.json());
// Combine resultsconst featured = analytics.results[0].resultSet.filter(item => config.featured_products.includes(item.product_id));Rollback when things go wrong:
View history to find when config was correct:
Rollback to before deletion (undo last 2 changes):
Or restore entire table to 1 hour ago:
Persistent memory for AI conversations:
// AI stores contextawait fetch('.../kv/agent:conversation:123?db=/data/agent.db', { method: 'PUT', body: JSON.stringify({ user_request: 'Build a todo app', plan: ['Create database', 'Build API', 'Create frontend'], progress: { completed: 0, total: 3 } })});
// AI retrieves context laterconst memory = await fetch('.../kv/agent:conversation:123?db=/data/agent.db') .then(r => r.json());
// AI continues from where it left offControl features via KV store:
// Set feature flagsawait fetch('.../kv/batch/set?db=/data/app.db', { method: 'POST', body: JSON.stringify({ batch: [ { key: 'feature:new-ui', value: true }, { key: 'feature:beta-api', value: false }, { key: 'feature:dark-mode', value: true } ] })});
// Check feature in appconst newUiEnabled = await fetch('.../kv/feature:new-ui?db=/data/app.db') .then(r => r.json());
if (newUiEnabled) { // Show new UI}Track API calls per user:
// Increment user's API call countawait fetch(`.../kv/api-calls:user-${userId}/incr?db=/data/limits.db`, { method: 'POST'});
// Get current countconst calls = await fetch(`.../kv/api-calls:user-${userId}?db=/data/limits.db`) .then(r => r.json());
if (calls > 1000) { return { error: 'Rate limit exceeded' };}Atomic increment prevents double-counting. Works correctly under high concurrency.
Cache expensive API responses:
const cacheKey = `cache:api:${endpoint}`;
// Check cacheconst cached = await fetch(`.../kv/${cacheKey}?db=/data/cache.db`) .then(r => r.json()) .catch(() => null);
if (cached) { return cached;}
// Fetch from APIconst data = await fetchFromAPI(endpoint);
// Store with 10-minute TTLawait fetch(`.../kv/${cacheKey}?db=/data/cache.db&ttl=600`, { method: 'PUT', body: JSON.stringify(data)});
return data;Track every data change:
// Make changeawait fetch('.../kv/user:1:email?db=/data/users.db', { method: 'PUT', body: JSON.stringify('new-email@example.com')});
// Later: Generate compliance reportconst history = await fetch('.../kv/user:1:email/history?db=/data/users.db') .then(r => r.json());
// Shows: who changed what, when, from what value to what value// Complete audit trail automaticallyDon’t overcomplicate:
// ❌ Overkill - SQL for simple configconst sql = 'SELECT value FROM config WHERE key = "timeout"';
// ✅ Simple - KV for simple dataconst timeout = await fetch('.../kv/config:timeout?db=/data/app.db') .then(r => r.json());Use SQL when you need: JOINs, complex queries, relationships
Use KV when you have: Simple key-value pairs, config, caching
100x faster than individual requests:
// ❌ Slow - 100 individual requestsfor (const user of users) { await fetch(`.../kv/user:${user.id}`, { method: 'PUT', body: JSON.stringify(user) });}
// ✅ Fast - 1 batch requestawait fetch('.../kv/batch/set?db=/data/app.db', { method: 'POST', body: JSON.stringify({ batch: users.map(user => ({ key: `user:${user.id}`, value: user })) })});Prevent race conditions:
// ✅ Correct - AtomicPOST /api/v1/sqlite/kv/counter/incr
// ❌ Wrong - Race conditionconst current = await GET /api/v1/sqlite/kv/counter;await PUT /api/v1/sqlite/kv/counter (current + 1);// Two clients do this simultaneously = counter wrongAuto-expire sessions, cache, tokens:
// Session expires in 24 hoursawait fetch('.../kv/session:xyz?db=/data/sessions.db&ttl=86400', { method: 'PUT', body: JSON.stringify({ user_id: '123' })});
// No cleanup job needed - automatic expiryKV store has built-in time-travel:
Before bulk update, note the current timestamp. Make changes, then if something went wrong:
Rollback entire table to timestamp:
Or rollback specific keys:
Yes! Use the SQL operations endpoint to execute standard SQL. The difference: instead of mysql -u user -p or psql, you make HTTP POST requests. Same SQL, different transport. Perfect for existing applications that need HTTP-native databases.
Similar concepts (key-value, atomic operations, TTL), different implementation. Redis is in-memory with persistence options. hoody-sqlite KV is SQLite-backed (disk-first). Redis has pub/sub. hoody-sqlite has time-travel and history. Both accessible via HTTP in Hoody containers.
Yes! Use SQLite Drive → by storing databases in /hoody/databases/. This special directory is automatically shared across containers with built-in locking and concurrency management. Multiple containers can safely read and write simultaneously.
Alternatively: One container owns a database in /data/, exposes it via hoody-sqlite HTTP endpoints, other containers query via HTTP. Both patterns work.
Each change stores: key, old value, new value, timestamp. For most use cases, overhead is minimal (5-10% of data size). You can disable history with ?history=false for ephemeral data like session tracking.
Absolutely! AI makes standard HTTP POST requests to execute SQL or KV operations. No database driver needed. LLMs understand HTTP natively—they can query data, analyze results, make decisions, all through HTTP.
SQLite supports databases up to 281 TB theoretically. Practically: keep databases under 100GB for optimal performance. For larger datasets, split across multiple databases or use container with more storage.
They execute at the database level in one step. incr does read-modify-write atomically—no other operation can interrupt between reading and writing. Multiple simultaneous requests are serialized by SQLite’s locking mechanism.
Yes! SQLite databases are single files. Copy the .db file via hoody-files service, container snapshots, or standard file operations. For hot backup (database in use), use SQLite’s VACUUM INTO or backup API.
No. When creating a database, use ?init_kv=true parameter. The KV table and indexes are created automatically. Or use the first KV operation and it creates the table if missing.
Problem: “Database is locked” errors during writes
Cause: SQLite allows one writer at a time per database
BEST Solution - Use SQLite Drive:
Hoody provides SQLite Drive →, a shared database system that eliminates locking issues for multi-container applications.
Store databases in /hoody/databases/ for shared access:
# Instead of: /data/app.db (single-container, can lock under concurrency)# Use: /hoody/databases/app.db (multi-container safe, no locking)Why /hoody/databases/ is better:
See: SQLite Drive → for complete setup and cross-container database access patterns.
Alternative Solutions (for single-container databases in /data/):
Use batch operations to reduce requests:
# Instead of 100 individual SETs# Use 1 batch SETPOST /api/v1/sqlite/kv/batch/setRetry with exponential backoff:
async function retryWrite(url, body, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fetch(url, { method: 'PUT', body }); } catch (e) { if (i < maxRetries - 1) { await new Promise(r => setTimeout(r, 100 * Math.pow(2, i))); } } }}Use writeahead logging (WAL mode):
-- Enable WAL for better concurrencyPRAGMA journal_mode=WAL;Problem: GET returns null for existing key
Check:
Verify key spelling (case-sensitive):
# ❌ Wrong: user:1# ✅ Correct: User:1 (if that's what you SET)Check key hasn’t expired:
# If TTL was set, key auto-deletes on expiryList all keys to verify:
-- Via SQLSELECT key FROM kv_store LIMIT 100;Problem: Rollback operation completes but data unchanged
Possible causes:
Missing confirm=yes on table rollback:
# Without confirm=yes, POST /kv/rollback returns a dry-run preview only# Add &confirm=yes to the query string to actually apply:POST /api/v1/sqlite/kv/rollback?to_timestamp=...&db=...&confirm=yesHistory disabled when data was written:
# If original SET had ?history=false# No history = can't rollbackTimestamp too far back:
# Rollback only works if history exists for that time# Check history first:GET /api/v1/sqlite/kv/{key}/historyWrong database file:
# Verify ?db= parameter points to correct fileProblem: Some keys in batch succeed, others fail
Reality: Batch operations are atomic—all succeed or all fail together. If you see partial success, you’re making multiple requests instead of one batch.
Verify:
// ✅ Correct - Single atomic batchPOST /api/v1/sqlite/kv/batch/set{ batch: [ { key: 'k1', value: v1 }, { key: 'k2', value: v2 } ]}
// ❌ Wrong - Two separate requestsPOST /api/v1/sqlite/kv/k1 (value: v1)POST /api/v1/sqlite/kv/k2 (value: v2)Check SQL syntax and parameters:
// Use parameterized queries{ query: 'SELECT * FROM users WHERE id = ?', values: [userId] // Not: `WHERE id = ${userId}` (SQL injection risk)}Verify database state:
# List tablesSELECT name FROM sqlite_master WHERE type='table';
# Check schemaPRAGMA table_info(users);Explore other data services:
Files
Access files across local storage and 60+ cloud providers through HTTP.
Exec
Turn scripts into HTTP endpoints—your code becomes an API automatically.
Master SQLite operations:
Your database is a URL.
SQL and KV in one.
Time-travel built-in.
Atomic by default.
Serverless forever.
This is how databases work in the HTTP era.