Storage and Data
Keelson’s storage model
Section titled “Keelson’s storage model”A Keelson app runs with an ephemeral local filesystem.
| Area | Persistence | Intended use |
|---|---|---|
| App directory (source code and dependencies) | Ephemeral and replaced by deployments | Application code and installed packages |
/tmp | Ephemeral and lost with the instance | Temporary scratch space |
Keelson does not mount or create /data, and an application cannot normally
create directories at the filesystem root. Use /tmp for bounded temporary
work. Like other writes to the container filesystem, those bytes consume
instance memory and disappear with the instance. Store durable relational data
in Keelson Managed SQLite (db.mode: libsql). For durable files, use an
appropriate Keelson SDK: the Files SDK for private files or the Media SDK for
content served to app members.
Where to put data
Section titled “Where to put data”Choose the durable interface that matches how the data is addressed and served.
- Application records, such as customers, projects, and events: Managed
SQLite (
db.mode: libsql) - Private files, such as state, settings, and generated reports that only the app reads: Files SDK
- Uploads and media served over HTTP: Media SDK
- Recreatable caches and temporary files:
/tmp; it is ephemeral and consumes instance memory
Do not use the local filesystem for persistence
Section titled “Do not use the local filesystem for persistence”Do not put anything on the local filesystem that you cannot afford to lose.
/data is not a platform-provided writable path. Choose storage by purpose:
- Business data and other durable records: Keelson Managed SQLite
(
db.mode: libsql) - Private key-addressed files: use the Files SDK and retain each key
- Published uploads and media: use the Media SDK and retain each returned ID
- Temporary files: use
/tmp, then delete scratch files promptly - Recreatable caches: dependencies and build output do not require durable storage
- Application logs: inspect them through Keelson’s logging features instead of accumulating log files locally
Keelson Managed SQLite (recommended)
Section titled “Keelson Managed SQLite (recommended)”For new apps, we recommend Keelson Managed SQLite. The platform provisions a dedicated managed SQLite-compatible libSQL database for each app and isolates it at the database level by workspace and app.
- No separate setup: connection details are injected as
KEELSON_DB_URLandKEELSON_DB_AUTH_TOKEN - Workspace isolation: each app’s credentials are scoped to its own database
- Durable commits: writes go to the primary and are durable when committed
- Regional placement: the primary is located in Tokyo
- Point-in-time recovery: available on every plan; the recovery window depends on the plan
JavaScript, TypeScript, and Python applications can connect with a libSQL client.
Enable the managed database explicitly in keelson.yaml; the platform does not
infer this choice from application code.
db: mode: libsqlUse an external database with db.mode: none
Section titled “Use an external database with db.mode: none”If you need PostgreSQL, MySQL, or an externally managed libSQL database, set
db.mode: none. Add its connection details as secrets yourself. Keelson does
not provision or inject credentials for an external database.
db: mode: noneFrameworks that normally default to file-based SQLite need a supported durable database integration. For Django, use this route with an external database, such as PostgreSQL. Keelson does not currently provide a supported Django backend for Managed SQLite. There is no durable deployment path for a local SQLite file.
File-based SQLite is not persistent
Section titled “File-based SQLite is not persistent”A SQLite database placed directly on the container filesystem is not persistent.
/data is neither mounted nor normally writable, and any writable local path
can be lost on restart or scale-to-zero. Deployment checks fail closed when they
detect an undeclared file-SQLite configuration.
Move applications using file-based clients such as better-sqlite3 or
sqlite3 to a libSQL client and use db.mode: libsql. A regenerable temporary
SQLite database may be declared under db.local_sqlite, but its path must be
under /tmp or use :memory: and it remains intentionally ephemeral.
db: mode: none local_sqlite: policy: ephemeral paths: - /tmp/cache.db reason: "Rebuilt from the upstream API after every cold start"Handle durable files
Section titled “Handle durable files”Use the Files SDK for private durable files that only the app can read. Install
it from the public package registry with npm install @keelsonhq/files,
pip install keelson-sdk (from keelson import files), or
go get github.com/keelsonhq/go-sdk (.../go-sdk/files).
Address each object by its Files SDK key and keep that key, ownership, and other application metadata in Managed SQLite. Files SDK objects have no URL and are not served over HTTP. See Files and media for operations, limits, and local-development behavior.
Handle published media
Section titled “Handle published media”Use the Media SDK for images, PDFs, attachments, and other durable content served
to app members. Install it from the public package registry with
npm install @keelsonhq/media, pip install keelson-sdk
(from keelson import media), or go get github.com/keelsonhq/go-sdk
(.../go-sdk/media).
Uploading returns an object ID. Keep that ID, ownership, and other application metadata alongside the related record in Managed SQLite. Media URLs use the app’s authentication and are available to app members; they are not public URLs. See Files and media for operations, limits, and local-development behavior.
Durability and recovery
Section titled “Durability and recovery”Restore points
Section titled “Restore points”Managed SQLite commits are stored by the managed database service rather than on the app’s ephemeral filesystem. Restarting or redeploying the app therefore does not discard committed database data.
Daily backups, manual snapshots, and point-in-time recovery (PITR) are available on every plan. Manual snapshots are limited to five per app per day.
| Plan | Daily backup retention | PITR window |
|---|---|---|
| Starter | 1 generation | 24 hours |
| Plus | 3 generations | 7 days |
| Team | 7 generations | 14 days |
Saved backups and any eligible PITR restore point can be downloaded on every plan. A point-in-time export creates a temporary database fork, produces a dump, and removes the fork after download. Export availability is not plan-gated.
PITR recovers a database to an eligible time within the plan window. Restore switches to a fork and retains the pre-restore generation for 72 hours, during which the restore can be undone.
What the recovery mechanism covers
Section titled “What the recovery mechanism covers”| Resource | Covered by Managed SQLite recovery? |
|---|---|
| Committed Managed SQLite (libSQL) data | Yes, through daily backups, manual snapshots, and PITR within the plan window |
Files under /tmp | No; /tmp is ephemeral |
| Objects in an external store | No; use that provider’s recovery features |
| Application source code | No; it is uploaded on each deployment |
| Environment variables | No; they are managed separately in the console |
Keep application code in version control such as Git. Put durable application records in Managed SQLite so they are eligible for the managed recovery path. Export critical data separately when your retention or recovery requirements extend beyond the PITR window.
Request point-in-time recovery
Section titled “Request point-in-time recovery”Choose a recovery time within the plan’s window when starting a point-in-time recovery. Recovery operates on one app’s database and does not expose data from other apps. The requested time must also fall within the recovery window made available by the managed database service.
When to use an external database or object store
Section titled “When to use an external database or object store”Managed SQLite works well for many internal applications, but an external service may be a better fit in these cases.
| Situation | Why | Possible choice |
|---|---|---|
| The dataset’s capacity or performance requirements outgrow Managed SQLite | The workload needs a different capacity or performance profile | PostgreSQL |
| The workload has very high concurrent write volume | SQLite serializes writers | PostgreSQL or MySQL |
| BI or core business systems must query the data directly | Other systems need independent access | Shared PostgreSQL or a data warehouse |
| Operations require specialized replication or recovery controls | The database needs capabilities outside the managed defaults | Managed PostgreSQL |
Common design patterns
Section titled “Common design patterns”Small business application
Section titled “Small business application”For a customer, inventory, or project management app, store records in Managed SQLite.
db: mode: libsqlBatch processing and daily aggregation
Section titled “Batch processing and daily aggregation”For CSV imports or scheduled aggregation, use /tmp as intermediate workspace
and save records in Managed SQLite. Write a downloadable report with the Files
SDK and keep its key in Managed SQLite.
AI application
Section titled “AI application”For an internal RAG or chat backend, store document metadata and embeddings in Managed SQLite. Store private source documents with the Files SDK and keep their keys in Managed SQLite.
Common mistakes
Section titled “Common mistakes”Saving data to a local file
Section titled “Saving data to a local file”Files written to the local container filesystem can be lost with the instance.
/tmp is the supported writable scratch location; /data is not a writable
platform mount. Put durable records in Managed SQLite and durable file objects
in the Files SDK or Media SDK, according to who should be able to read them.
# Wrong: local paths are ephemeral, and /data is not a writable mount.db_path = "./data.db"db_path = "/tmp/data.db"db_path = "/data/main.db"
# Correct: connect to Managed SQLite with the injected credentials.import osimport libsql
conn = libsql.connect( database=os.environ["KEELSON_DB_URL"], auth_token=os.environ["KEELSON_DB_AUTH_TOKEN"],)Using a file-SQLite client
Section titled “Using a file-SQLite client”An app that tries to create a database under /data with better-sqlite3,
sqlite3, or a similar file client cannot rely on that path being writable or
durable and fails deployment checks. Migrate to a libSQL client and Managed
SQLite.
Misunderstanding recovery coverage
Section titled “Misunderstanding recovery coverage”Daily backups, manual snapshots, PITR, and exports cover committed Managed SQLite data, not local files. Check the daily-backup retention and PITR window for your plan. Download a restore point when you need an independently retained copy.
Letting temporary storage grow without bounds
Section titled “Letting temporary storage grow without bounds”Remove scratch files when processing finishes. Writable container files consume
instance memory. Unbounded writes can exhaust that memory and crash the
instance. Keep temporary work in /tmp; Keelson does not assign /data a
separate disk capacity.
Hard-coding a SQLite path
Section titled “Hard-coding a SQLite path”Code such as sqlite3.connect("/data/main.db") assumes durable local storage that
Keelson does not provide. Read the injected Managed SQLite credentials and use a
libSQL client instead.
Sample code
Section titled “Sample code”Python: write to Managed SQLite with libSQL
Section titled “Python: write to Managed SQLite with libSQL”import osimport libsql
def get_db(): conn = libsql.connect( database=os.environ["KEELSON_DB_URL"], auth_token=os.environ["KEELSON_DB_AUTH_TOKEN"], ) conn.execute(""" CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) return conn
conn = get_db()conn.execute("INSERT INTO items (name) VALUES (?)", ("Example item",))conn.commit()The corresponding keelson.yaml enables Managed SQLite:
slug: my-appruntime: python-slimcommand: "python app.py"db: mode: libsqlDeclare the libSQL client in requirements.txt or pyproject.toml. Keelson
installs dependencies during the image build. The startup command must only
start the app, and the app must listen on the PORT value Keelson injects.
Node.js: write to Managed SQLite with libSQL
Section titled “Node.js: write to Managed SQLite with libSQL”import { createClient } from "@libsql/client";
const db = createClient({ url: process.env.KEELSON_DB_URL, authToken: process.env.KEELSON_DB_AUTH_TOKEN,});
await db.execute(` CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`);await db.execute({ sql: "INSERT INTO items (name) VALUES (?)", args: ["Example item"],});When Managed SQLite is a good fit
Section titled “When Managed SQLite is a good fit”- Business data for internal apps, such as customers, projects, and stock
- AI-generated apps that need a database immediately, without provisioning an external database
- Many small apps, each with database-level app and workspace isolation
When Managed SQLite may not be a good fit
Section titled “When Managed SQLite may not be a good fit”- Very large datasets where an external database offers a better capacity and performance profile
- Concurrent access from several independent systems, rather than access primarily from the deployed app
- Specialized recovery or replication requirements beyond the plan’s managed capabilities
- Direct integration with BI or core business systems that need their own database connection
Frequently asked questions
Section titled “Frequently asked questions”What is the storage limit?
Section titled “What is the storage limit?”Managed SQLite: No fixed limit by plan. The underlying platform’s technical limits still apply. If we detect load far beyond normal business use, or use that affects platform stability, other customers, or our reasonable operating costs, we may restrict or suspend the application. In an emergency, we may impose restrictions without prior notice.
The separate plan storage quota, from 10 GB on Starter to 50 GB on Team, is a workspace-wide aggregate. It counts deployment artifacts, stored file data, snapshot dumps and backups, and metered app data usage. It does not count the Managed SQLite database itself; Managed SQLite usage is metered separately. Purchasing additional workspace storage does not change the database’s separate technical limits. The container filesystem does not provide durable capacity.
Does a redeploy delete my data?
Section titled “Does a redeploy delete my data?”Local container data can be lost. Managed SQLite data remains available after a redeploy because committed data is stored outside the app’s ephemeral filesystem. Eligible recovery times are limited by the plan’s PITR window.
Can I use a database other than SQLite?
Section titled “Can I use a database other than SQLite?”Yes. Managed SQLite is the default recommendation, but an app can connect to an
external PostgreSQL, MySQL, or other database over an outbound connection. Set
db.mode: none and supply its credentials as secrets.
How can another app access my files?
Section titled “How can another app access my files?”Local filesystems and Files SDK objects are isolated. If another app needs private data, expose an authenticated API from the app that owns the Files SDK object. Use the Media SDK only for content intended to be served to app members; its URL is not public and uses the app’s authentication.
How long does point-in-time recovery take?
Section titled “How long does point-in-time recovery take?”Recovery time depends on data volume and the managed database service. Do not assume a fixed recovery time when defining an application’s recovery objective.
Example prompts for an AI agent
Section titled “Example prompts for an AI agent”Use Keelson Managed SQLite (libSQL) and declare
db.mode: libsql.
Read
KEELSON_DB_URLandKEELSON_DB_AUTH_TOKENfrom the environment and use a libSQL client. Do not use file-based SQLite.
Store private durable files with the Files SDK and keep each key in Managed SQLite. Use the Media SDK for content served to app members.
Use
/tmpfor bounded scratch data. Do not write to/data; Keelson does not provide it as a writable platform mount.