SQLite Driver
Section titled “SQLite Driver”A SQLite database stored in /hoody/databases/ is concurrent-write-safe. Several containers can write to it at the same time without corrupting the file, and the only change to your code is the path.
AI-generated code hits this problem often, because parallel tasks tend to open the same database and race each other. Under /hoody/databases/ the FUSE mount coordinates those writes instead of leaving the calling code to get the locking right.
API Endpoints Summary
Section titled “API Endpoints Summary”Database access:
- Hoody SQLite API - HTTP-based KV store access
- SQL Operations - Execute SQL transactions via HTTP
Direct access:
- Hoody Terminal - Use standard
sqlite3command - SSH Access - Reach the database files over SSH
Why concurrent SQLite writes fail
Section titled “Why concurrent SQLite writes fail”Corruption from concurrent writes
Section titled “Corruption from concurrent writes”AI agents frequently generate code shaped like this, where parallel tasks share one database file:
# AI-generated code (common pattern)import sqlite3
# Multiple AI tasks running simultaneouslydef process_task(task_id): conn = sqlite3.connect('/app/data.db') # Same database cursor = conn.cursor() cursor.execute("INSERT INTO tasks VALUES (?, 'completed')", (task_id,)) conn.commit() conn.close()
# Task 1, 2, 3... all running in parallel# Result: "database is locked" error or corruptionUnder /hoody/databases/, the same code runs without race conditions or corruption.
Access from multiple containers
Section titled “Access from multiple containers”On an ordinary path, two containers writing at once collide:
# Container A writes to databasesqlite3 /app/data.db "INSERT INTO users..."
# Container B writes simultaneouslysqlite3 /app/data.db "INSERT INTO posts..."
# Result: "database is locked" error# or worse: database corruptionSQLite locks at the file level. When several processes, or several containers, try to write, one is locked out or the database is corrupted.
The /hoody/databases/ mount
Section titled “The /hoody/databases/ mount”Store the database in /hoody/databases/ instead:
# Container Asqlite3 /hoody/databases/shared.db "INSERT INTO users..."
# Container B (simultaneously)sqlite3 /hoody/databases/shared.db "INSERT INTO posts..."
# Result: Both succeed# No locking errors# No corruption# Zero code changes/hoody/databases/ is a FUSE mount on the host level that implements concurrent write handling for SQLite databases.
How it works
Section titled “How it works”Automatic availability
Section titled “Automatic availability”Every container has /hoody/databases/ from the start:
# No setup required - just use itls /hoody/databases/# Directory exists and is ready to useThe directory is present in every container and works immediately. There is nothing to configure and no mount command to run.
Host-level FUSE mount
Section titled “Host-level FUSE mount”┌─────────────────────────────────────────┐│ Physical Host Server ││ ││ ┌─────────────────────────────────┐ ││ │ Special FUSE Driver Layer │ ││ │ (concurrent write coordination)│ ││ └─────────────────────────────────┘ ││ ↓ ││ ┌─────────────────────────────────┐ ││ │ Actual Database Storage │ ││ └─────────────────────────────────┘ ││ ││ Container A Container B Container C│ ↓ ↓ ↓│ /hoody/databases/ /hoody/databases/ ...└─────────────────────────────────────────┘The FUSE layer intercepts all writes and coordinates them at the filesystem level, so simultaneous writes do not conflict.
Usage patterns
Section titled “Usage patterns”Drop-in replacement
Section titled “Drop-in replacement”Your application needs no code change beyond the database path:
# Risk of corruption with multiple containersimport sqlite3conn = sqlite3.connect('/app/database.db')cursor = conn.cursor()cursor.execute("INSERT INTO users VALUES (?, ?)", (1, 'Alice'))conn.commit()# Concurrent-write-safe - only the path changedimport sqlite3conn = sqlite3.connect('/hoody/databases/database.db')cursor = conn.cursor()cursor.execute("INSERT INTO users VALUES (?, ?)", (1, 'Alice'))conn.commit()Only the path changed. Everything else is identical.
This works with any program that uses sqlite3:
- Python (sqlite3 module)
- Node.js (better-sqlite3, sqlite3)
- Go (mattn/go-sqlite3)
- PHP (PDO SQLite)
- Any language with SQLite bindings
Native access plus the HTTP API
Section titled “Native access plus the HTTP API”One database is reachable over HTTP and through native sqlite3 at the same time:
# Container A: Native sqlite3 (fast, direct access)sqlite3 /hoody/databases/app.db "SELECT * FROM users"
# Container B: hoody-sqlite HTTP API (remote access)curl "https://...-sqlite-1.../api/v1/sqlite/db?db=/hoody/databases/app.db" \ -d '{"transaction": [{"query": "SELECT * FROM users"}]}'
# Same database, two access methodsEach path suits different work:
- Native access for performance-critical operations
- HTTP access for remote monitoring, web dashboards, API integrations
- Both methods work simultaneously without conflicts
See: Hoody SQLite for complete HTTP API documentation.
Capabilities and limits
Section titled “Capabilities and limits”Capabilities
Section titled “Capabilities”Concurrent write safety:
- Several containers can write to the same database
- Several processes inside one container can write
- No “database is locked” errors
- No corruption from simultaneous writes
No configuration:
- Available in every container
- Nothing to set up or mount
- Works with the standard sqlite3 library
- No code change beyond the path
Cross-container databases:
- One database file shared across many containers
- Every container sees the same data instantly
- Suits multi-service architectures
- Removes the need for a separate database server
Limits
Section titled “Limits”Not a replication system:
- Concurrent writes are coordinated safely
- Data is not replicated to other hosts
- Backups do not happen automatically
- There is no failover
Single host only:
- Containers on the same server can share databases
- Containers on different servers cannot, yet
- Each server has its own
/hoody/databases/space
No automatic backups:
- Snapshot or copy the databases yourself
- The FUSE layer gives safety, not redundancy
Use Cases
Section titled “Use Cases”Multi-service application
Section titled “Multi-service application”One application database shared by the frontend, the backend, and the workers:
# Container 1 (API Server)# Handles user registration, authentication
# Container 2 (Background Worker)# /hoody/databases/app.db (same database)# Processes jobs, updates status
# Container 3 (Reporting Dashboard)# /hoody/databases/app.db (same database)# Read-only queries for reporting
# All three write safely to the same databaseThe usual approach is a PostgreSQL or MySQL server that every container reaches over the network, which adds a service to operate and network overhead. Here it is one SQLite file in /hoody/databases/, opened directly by each container.
Development database
Section titled “Development database”Two developers write to one database during active development:
# Developer A's container writes schema changessqlite3 /hoody/databases/dev.db < migrations/001.sql
# Developer B's container writes seed data (simultaneously)sqlite3 /hoody/databases/dev.db < seeds/users.sql
# No conflicts - both succeedAnalytics pipeline
Section titled “Analytics pipeline”Ingestion and real-time queries run against the same database:
# Container A: Ingest metricswhile true; do sqlite3 /hoody/databases/metrics.db \ "INSERT INTO events VALUES (datetime('now'), '$data')"done
# Container B: Query metrics (simultaneously)sqlite3 /hoody/databases/metrics.db \ "SELECT COUNT(*) FROM events WHERE timestamp > datetime('now', '-1 hour')"
# No blocking - queries run while inserts happenIntegration with hoody-sqlite
Section titled “Integration with hoody-sqlite”The same databases are reachable over HTTP through hoody-sqlite.
hoodyCLI prerequisite. The CLI tabs below invokehoody db …. If you haven’t already, install the CLI (npm i -g hoody-sdk) and authenticate (hoody auth login) so the CLI picks up your Hoody token and default base URL.
SQL transactions
Section titled “SQL transactions”# Execute SQL transaction via CLIhoody db exec-transaction -c $CONTAINER --db /hoody/databases/app.db \ --transaction '[{"query": "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100"}]'
# Create a new databasehoody db create -c $CONTAINER --path /hoody/databases/analytics.dbconst containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER});
// Execute SQL transaction. Pass the body (transaction array) as the first// argument and the query params (db) as the second. Each item sets exactly one of// `query` (SELECT-style, returns rows) or `statement` (alias `sql`, for// INSERT/UPDATE/DELETE/DDL, returns metadata like rowsUpdated), plus optional// `values`. Results are returned per item.const result = await containerClient.sqlite.database.executeTransaction( { transaction: [ { query: 'SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100' } ] }, { db: '/hoody/databases/app.db' });console.log(result.data); // Query results# Execute SQL transaction. Hoody Proxy owns Kit auth, so a raw request carries# whatever credential the effective container-or-project policy demands, and# nothing at all by default, since a container with no proxy permissions# configured is open to anyone holding the unguessable URL. $PROXY_TOKEN below is# the bearer-token case; the SDK/CLI attach it for you. Policies can require a# different credential entirely. See /foundation/proxy/permissions/.curl -X POST "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.com/api/v1/sqlite/db?db=/hoody/databases/app.db" \ -H "Authorization: Bearer $PROXY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"transaction": [{"query": "SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100"}]}'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
Runs the query and returns the last 100 log rows. Carries a bearer token because this example assumes the container’s proxy policy requires one; by default none does.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER.containers.hoody.com/api/v1/sqlite/db?db=/hoody/databases/app.db&method=POST&bearer_token=TOKEN&json={"transaction":[{"query":"SELECT%20*%20FROM%20logs%20ORDER%20BY%20timestamp%20DESC%20LIMIT%20100"}]}&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.
KV store operations
Section titled “KV store operations”# Set a key-value pairhoody kv set user:1 -c $CONTAINER --db /hoody/databases/app.db \ --body '{"name": "Alice", "email": "alice@example.com"}'
# Get a value by keyhoody kv get user:1 -c $CONTAINER --db /hoody/databases/app.db// Set value. Pass key, db, and data (the raw value) in a single options object.const db = '/hoody/databases/app.db';await containerClient.sqlite.kvStore.set('user:1', '{"name": "Alice", "email": "alice@example.com"}', { db });
// Get value. Pass key and db in a single options object. The SDK returns the// standard { statusCode, message, data } envelope; the stored value is in `data`.const res = await containerClient.sqlite.kvStore.get('user:1', { db });console.log(res.data); // { name: 'Alice', email: 'alice@example.com' }# $PROXY_TOKEN is the proxy-minted token for this container-proxy path. Send it# only when the container's proxy policy asks for a bearer token. By default no# permissions are configured and no credential is needed at all.# See /foundation/proxy/permissions/.
# Set valuecurl -X PUT "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.com/api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db" \ -H "Authorization: Bearer $PROXY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Alice", "email": "alice@example.com"}'
# Get valuecurl "https://$PROJECT-$CONTAINER-sqlite-1.$SERVER.containers.hoody.com/api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db" \ -H "Authorization: Bearer $PROXY_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
Sets then reads back the same key. Both links carry a bearer token because this example assumes the container’s proxy policy requires one; by default none does.
# Set
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER.containers.hoody.com/api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db&method=PUT&bearer_token=TOKEN&json={"name":"Alice","email":"alice@example.com"}&response=transparent
# Get
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER.containers.hoody.com/api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db&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.
Native and HTTP together
Section titled “Native and HTTP together”// Backend container: Native sqlite3 for bulk inserts (faster)const db = require('better-sqlite3')('/hoody/databases/app.db');db.prepare('INSERT INTO logs VALUES (?, ?)').run(timestamp, message);
// Frontend container: HTTP API for remote queries. No Authorization header here// because this container has no proxy permissions configured (the default).// Add the credential its policy requires once you configure one.const response = await fetch( 'https://...-sqlite-1.../api/v1/sqlite/db?db=/hoody/databases/app.db', { method: 'POST', body: JSON.stringify({ transaction: [{ query: 'SELECT * FROM logs ORDER BY timestamp DESC LIMIT 100' }] }) });Writes stay on the fast native path while reads remain available over HTTP.
# Hoody SQLite KV Store API (even simpler)# No auth header: this container has no proxy permissions configured (the default).# Set valuecurl -X PUT "https://...-sqlite-1.../api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db" \ -d '{"name": "Alice", "email": "alice@example.com"}'
# Get valuecurl "https://...-sqlite-1.../api/v1/sqlite/kv/user:1?db=/hoody/databases/app.db"
# Concurrent writes automatically safeEverything runs through the API, so the sqlite3 CLI is never used.
See: Hoody SQLite KV Store and SQL Operations
Best Practices
Section titled “Best Practices”Keep SQLite databases in /hoody/databases/
Section titled “Keep SQLite databases in /hoody/databases/”A database that more than one container will open must never live outside this directory:
# Correct - concurrent write safe/hoody/databases/production.db/hoody/databases/cache.db/hoody/databases/analytics.db
# Wrong - risk of corruption/hoody/storage/production.db/var/lib/myapp/data.db/tmp/cache.dbUse standard SQLite libraries
Section titled “Use standard SQLite libraries”No special driver is needed:
# Works with standard libraryimport sqlite3conn = sqlite3.connect('/hoody/databases/app.db')The FUSE mount handles the concurrent-write safety, so your code stays standard.
Back up databases regularly
Section titled “Back up databases regularly”Concurrent-write safety is not a backup:
# Via snapshot (captures entire container state)POST /api/v1/containers/{id}/snapshots{"alias": "before-migration"}
# Or copy database filecp /hoody/databases/production.db /hoody/storage/backups/prod-2025-11-10.db
# Or use sqlite3 backup commandsqlite3 /hoody/databases/production.db ".backup /hoody/storage/backups/backup.db"Monitor database size
Section titled “Monitor database size”# Check database sizesdu -sh /hoody/databases/*
# Vacuum to compactsqlite3 /hoody/databases/app.db "VACUUM"
# Set up auto-vacuumsqlite3 /hoody/databases/app.db "PRAGMA auto_vacuum = FULL"Add indexes for query performance
Section titled “Add indexes for query performance”Concurrent writes are safe, but query speed still depends on indexes:
-- Create indexes for frequently queried columnsCREATE INDEX idx_users_email ON users(email);CREATE INDEX idx_posts_author ON posts(author_id, created_at);
-- Analyze for query plannerANALYZE;Useful Questions
Section titled “Useful Questions”Do I need to do anything to enable /hoody/databases/?
Section titled “Do I need to do anything to enable /hoody/databases/?”No. The directory is available in every container. Start using it:
sqlite3 /hoody/databases/myapp.db "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"Can containers on different servers share databases?
Section titled “Can containers on different servers share databases?”Not yet. /hoody/databases/ is host-level, so sharing works between containers on the same physical server.
Across servers, use the hoody-sqlite HTTP API to reach a database remotely.
Does this work with PostgreSQL or MySQL?
Section titled “Does this work with PostgreSQL or MySQL?”No. The FUSE mount handles SQLite’s file-level locking protocol and nothing else. Run PostgreSQL or MySQL as services in containers and connect to them over the network.
What if my database gets really large (>10GB)?
Section titled “What if my database gets really large (>10GB)?”The FUSE mount works regardless of database size, but keep databases under 100GB for the best performance. For larger datasets, consider:
- Sharding across multiple databases
- Indexing aggressively
- Moving to a container with more storage
- Running a dedicated PostgreSQL container
Can I use WAL mode with /hoody/databases/?
Section titled “Can I use WAL mode with /hoody/databases/?”Yes. SQLite’s Write-Ahead Logging (WAL) mode works with the concurrent-write FUSE mount:
PRAGMA journal_mode = WAL;WAL adds concurrent read performance on top of the mount’s concurrent write safety.
Does the HTTP API require /hoody/databases/?
Section titled “Does the HTTP API require /hoody/databases/?”No. hoody-sqlite can access databases anywhere in the container filesystem:
# Works - in /hoody/databases/curl "...?db=/hoody/databases/app.db"
# Also works - anywhere elsecurl "...?db=/home/user/data/test.db"Concurrent write safety across containers is the exception: for that, the database must be in /hoody/databases/.
Troubleshooting
Section titled “Troubleshooting””Database is locked” errors
Section titled “”Database is locked” errors”Lock errors can still appear while using /hoody/databases/. The usual causes:
-
Long-running transactions:
-- Wrong: Holding write lock too longBEGIN EXCLUSIVE;-- Complex operations taking secondsCOMMIT;-- Correct: Break into smaller transactionsBEGIN; INSERT ...; COMMIT;BEGIN; INSERT ...; COMMIT; -
Busy timeout too low:
# Increase timeoutconn = sqlite3.connect('/hoody/databases/app.db')conn.execute('PRAGMA busy_timeout = 5000') # 5 seconds -
Very high write concurrency:
- The FUSE mount has limits
- Consider connection pooling
- Or switch to WAL mode
Database file not found
Section titled “Database file not found”For sqlite3: cannot open database, check the directory, create the database if it is missing, and check permissions:
# Verify directory existsls -la /hoody/databases/
# Create database if neededsqlite3 /hoody/databases/newdb.db "CREATE TABLE test (id INTEGER)"
# Check permissionsls -la /hoody/databases/newdb.db# Should be: -rw-r--r-- root rootSlower queries than expected
Section titled “Slower queries than expected”If queries run slower in /hoody/databases/ than on the regular filesystem, work through these:
-
Enable WAL mode:
PRAGMA journal_mode = WAL;PRAGMA synchronous = NORMAL; -
Use indexes:
CREATE INDEX idx_query ON table(column);ANALYZE; -
Increase cache size:
PRAGMA cache_size = -64000; -- 64MB cache -
Batch operations:
BEGIN;-- Multiple INSERTsCOMMIT;
Concurrent write architecture
Section titled “Concurrent write architecture”The FUSE mount coordinates writes over time like this:
Time Container A FUSE Layer Container B─────────────────────────────────────────────────────────────────────T1 BEGIN TRANSACTION ← Request write lock (waiting)T2 INSERT INTO users Lock granted to A (waiting)T3 INSERT INTO posts Buffering A's writes (waiting)T4 COMMIT Flushing A's changes (waiting)T5 (done) Release lock BEGIN TRANSACTIONT6 (ready for next) Grant lock to B ← INSERT INTO logsT7 Buffering B's writes COMMITT8 Release lock (done)The FUSE layer serializes competing writes while allowing concurrent reads.
What’s Next
Section titled “What’s Next”Storage:
- Container Storage → -
/hoody/storageand the container filesystem - Mount Locally → - Access container files via SFTP/WebDAV
- Cloud Storage → - Connect 62 cloud providers
- Shared Storage → - Share directories between containers
- /ramdisk → - RAM-backed storage
Database access:
- Hoody SQLite KV Store → - HTTP-based key-value operations
- SQL Operations → - Execute transactions via HTTP
- Hoody Terminal → - Use sqlite3 command directly