Skip to content
Console →
Website →
Asking an AI? Paste this URL https://keelson.dev/llms.txt

Files and media

The local filesystem is ephemeral, so save files that need to persist through an SDK. There are two SDKs for different purposes.

Files SDKMedia SDK
What to storeFiles that the app names and updates itself (settings, state, caches, generated CSV files)Images, PDFs, and attachments uploaded by users or generated by the app
Who can read themOnly the app. They have no URL and are not served over HTTPMembers of the app. They are served at URLs
OverwritingSupported (about once per second per key)Not supported (one ID per file, write once)
Limit10 MiB per file50 MiB per file

Neither SDK requires a declaration in keelson.yaml. A file is persistent when the save completes; there is no background synchronization. For structured data that is read and written on each request, use Managed SQLite.

LanguagePackage
Node.jsnpm install @keelsonhq/files @keelsonhq/media
Pythonpip install keelson-sdk (from keelson import files, media)
Gogo get github.com/keelsonhq/go-sdk (.../go-sdk/files, .../go-sdk/media)

Keys are slash-separated relative paths (settings.json, reports/2026-08.csv). The SDK has four operations: write, read, delete, and list. If a key does not exist, read returns null / None.

import * as files from "@keelsonhq/files";
await files.write("settings.json", JSON.stringify({ theme: "dark" }));
const raw = await files.read("settings.json"); // Uint8Array | null
const settings = raw ? JSON.parse(new TextDecoder().decode(raw)) : {};
const keys = await files.list("reports/"); // ["reports/2026-08.csv", ...]
await files.delete("reports/2026-07.csv");
import json
from keelson import files
files.write("settings.json", json.dumps({"theme": "dark"}))
settings = json.loads(files.read("settings.json") or "{}") # read() -> bytes | None
keys = files.list("reports/")
files.delete("reports/2026-07.csv")

A common use is to replace writes to local files with SDK calls. Change open("seen_urls.json", "w") to files.write("seen_urls.json", ...), and the Scheduled Job and web app can share the file.

Uploading with put returns an ID (ULID). Store the ID in the database, then use url(id) to construct a URL when displaying the file.

import * as media from "@keelsonhq/media";
// Upload
const id = await media.put(req.file.buffer, {
contentType: req.file.mimetype,
filename: req.file.originalname,
});
await db.execute({ sql: "UPDATE items SET photo_id = ? WHERE id = ?", args: [id, itemId] });
// Display
const src = media.url(id); // "/__keelson/media/<id>"
from keelson import media
file_id = media.put(upload.read(), content_type=upload.mimetype, filename=upload.filename)
src = media.url(file_id) # "/__keelson/media/<id>"
  • The URL is /__keelson/media/<id>. It uses the same authentication as the app, so only members with view permission for that app can open it. It is not a public URL
  • The SDK provides get(id) for content, stat(id) for the Content-Type and size, as well as exists(id) and delete(id)
  • If no Content-Type is specified, it is determined from the filename extension

Both SDKs automatically use local mode outside Keelson.

  • Files: writes actual files under ./.keelson/files/. Add .keelson/ to .gitignore
  • Media: writes files under ./media/ (configurable with MEDIA_DIR)

The SDKs use the KEELSON_MODE environment variable to determine whether they are running on Keelson. No configuration is required.

The Node.js Files SDK does not support local mode on Windows. Use WSL2 or a Linux devcontainer. Python and Go do not have this restriction.

  • You cannot list, download, or replace an app’s files from the console or CLI. If users need to retrieve files, implement an authenticated download endpoint in the app (Keelson authentication applies to regular routes)
  • Save files that exceed the limits directly from the app to an external S3-compatible object store (outbound traffic is unrestricted)

Files stored with Files / Media count toward the workspace storage capacity described in Plans and limits.