Keelson’s supported runtimes and constraints are collected on one page in the Keelson Deploy Spec. Give the following URL to an AI agent to have it check whether an application you created can be deployed to Keelson:
A supported terminal — macOS or Linux with curl and openssl, or Windows 10/11 or Windows Server 2016+ with Windows PowerShell 5.1 or later. Windows PowerShell 5.1 and its CNG cryptography support are included with supported Windows versions.
Both installers verify the release signature (ECDSA P-256) and the sha256 of the binary before installing it. If verification fails, the existing CLI is left unchanged. The default destination is ~/.keelson/bin/keelson on macOS and Linux, or %USERPROFILE%\.keelson\bin\keelson.exe on Windows. The Windows installer uses the operating system’s CNG cryptography and does not require OpenSSL.
The installer does not modify your PATH. On macOS or Linux, if keelson is not found, have the CLI append it to your shell config:
Terminal window
~/.keelson/bin/keelson doctor --fix-path
This only rewrites the config file, so your current shell is not affected yet. Restart the shell, or run the line doctor prints:
Terminal window
export PATH="$HOME/.keelson/bin:$PATH"
If you would rather not touch your shell config, run just the export — it applies for the current shell only.
On Windows, add %USERPROFILE%\.keelson\bin to your user Path in Edit environment variables for your account, then open a new PowerShell window. To use it only in the current PowerShell session, run:
The sample app requires an API key from at least one supported AI provider:
GEMINI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY. Before deploying,
create an env file in the sample app folder with one of those keys:
OPENAI_API_KEY=your-api-key
For the first deployment, the agent will need to run:
Keelson lets your AI agent handle deploys and deploy status (log inspection) for you. The Skill it uses is bundled with the CLI — there is nothing extra to download.
From inside the sample app folder, run the command for the agent you use:
Terminal window
keelsoninstall-agentclaude-code
Target
Destination
claude-code
.claude/skills/keelson/ in the current directory (--global installs to ~/.claude/skills/keelson/)
codex
~/.codex/skills/keelson/
cursor
~/.cursor/skills/keelson/
claude-code installs into the current directory by default, so the Skill lands in the sample app folder you just entered. Pass --global to use it in every project instead.
Open the extracted folder in your AI agent (Claude Code, for example).
No long commands are needed. Type one instruction into your agent:
Example:
“Use the Skill to deploy this app to Keelson with the first-deployment command shown above.”
The agent reads keelson.yaml and starts the build and deploy. A deploy of this sample usually takes 3–6 minutes; most of that time is the build, including dependency installation. During the health check, the CLI shows whether it is waiting for permissions, routing, or the app to respond. When it finishes, the agent reports a public URL:
A public URL is assigned when the app is created, so seeing the URL alone does
not prove that the deployment succeeded. A command running without an
interactive terminal can return while the deployment is still in progress, and
the deployment may fail afterward. Before opening the URL, check the deployment
ID reported by the deploy command:
Terminal window
keelsonstatus<deploy-id>
If the deployment failed, this command shows the reason and the next action.
Keelson is a runtime platform for securely publishing applications that are shared within an organization or team.
You can focus on developing the application itself while Keelson handles the systems required to publish, authenticate, and share it. This page explains how those responsibilities are divided.
When a user opens the URL of an application deployed on Keelson, the request is processed in this order:
Keelson identifies the application associated with the public URL.
The Keelson Proxy checks whether the user is logged in.
Keelson checks whether the user has permission to access the application.
Only an authorized request is forwarded to the application.
The application’s response is returned to the user.
The important point is that authentication and access-control decisions happen outside the application itself. Application developers can therefore publish applications intended for sharing without building a login screen or session management from scratch each time.
Keelson separates authentication and access control from the application. The application can concentrate on business logic, while the platform applies security consistently when the application is shared.
Why does the application not need to implement authentication?
Login protection is enabled as soon as an application is deployed to Keelson. We call this Security by Default.
Security is built in from the beginning instead of added later.
The application is protected when it is published, rather than requiring authentication to be added after it works.
The login screen shown in a private browser window during the Quickstart is the result of this protection.
This is why Keelson is more than application hosting: it is a runtime platform for internal applications.
Keelson provides login protection and access control at the application’s entry point. If an application needs additional controls based on its own business rules, implement those controls within the application.
At first, you only need to focus on deploying and running the application.
You can configure Keelson incrementally. The experience covered in the Quickstart is the entry point with minimal configuration. Add settings such as these when you need them:
It depends on the structure. Static frontend output can be deployed directly in assets mode. Apps with server-side processing, such as Next.js, are deployed as servers. Parts that depend on external services such as Supabase can keep using those connections. See the bring your app guide for how to assess your app.
Not currently. Streamlit requires WebSockets, which do not fit Keelson’s execution model. Gradio is under evaluation. See the compatibility table in Supported apps and constraints.
After the 14-day free trial. Starter and Plus can start without a card; if you do not add one before the trial ends, it ends without a charge. Team requires a card when starting the trial. See Billing and subscriptions.
Only the builders count. Owners, Admins, and Developers together use Developers seats: Starter includes 1, Plus 2, and Team 3. App Users do not affect the price, regardless of their number. Two builders need Plus or above. Also check the concurrent-app allowance: Starter 1, Plus 2, and Team 4. See Plans and limits.
If I store 10 apps but usually use only some of them, which limit applies?
Paid subscriptions have no stored-app limit; only the number running concurrently is limited. Only apps accessed within the last 5 minutes occupy a slot. Unused apps sleep and release their slots. During the trial, stored apps are also limited to 3 (apps already deployed as static sites do not count). See Plans and limits.
What do users see when all concurrent-app slots are occupied?
Accessing a sleeping app shows an HTTP 503 information page. A slot becomes available after an app occupying it receives no access for about 5 minutes. No slots become available while the other apps remain in use. You can reserve a slot for an important app with priority start on Plus and above.
No. An app is visible only to signed-in workspace members who have permission to view it. People who are not signed in see the login page. To share an app with someone outside your organization, invite them as a member (App Users are unlimited). See Authentication and login.
The app and its Managed SQLite database are placed in the region selected when the app is created (Japan or the US West Coast). Selecting Japan keeps them in Japan. The workspace’s default region is determined by its billing country and can be changed in settings. Every plan includes daily backups, manual snapshots, point-in-time recovery (PITR), and downloads. Files stored through the Files / Media SDK are not covered by backups. See Databases.
What happens to apps and data when a trial ends, I change plans, or I cancel?
Apps and data are not deleted immediately in any of these cases. When a subscription ends, new deployments and app starts are blocked, and apps are automatically deleted after a 30-day grace period. Resubscribe during the grace period to keep them. A plan downgrade takes effect at the end of the billing period, and apps exceeding the allowance cannot start. See Billing and subscriptions.
No. They run only while processing requests or executing declared scheduled jobs. They stop when there is no access and start on the next request. Persistent timers and work performed after returning a response do not run. Read How apps run before building.
Does Keelson’s price include API usage fees for services such as OpenAI inside my app?
No. Calls from your app to external APIs are billed under your agreement with that API provider. Keelson only passes API keys securely as secrets; it does not restrict or mediate external communications. See External integrations.
A form, list view, and database make up a typical CRUD business application.
Keelson Managed SQLite (libSQL, configured with db.mode: libsql) can provide
the durable database without requiring a separately managed database server.
A text-search interface backed by an API can make company knowledge searchable.
Keelson places authentication in front of the app so it can be shared safely
inside the organization.
An upload, processing step, and download can automate repetitive data work. A
Keelson URL makes the finished tool available to non-developers in a browser.
A form, status tracking, and notifications can support a small approval flow.
Combine it with workspace membership and app access settings to control who can
use it.
An app can consist only of cron jobs. Sales aggregation, log analysis, and
notification delivery do not require a user interface or a resident web server.
Web app — Starts an HTTP server and serves a browser interface
API server — Provides a JSON or other HTTP API
Scheduled job (cron) — Runs a command on a schedule. Background work such
as draining accumulated tasks is also expressed as a scheduled job
Examples of compatible frameworks include FastAPI, Flask, Express, Next.js,
Hono, and Gin. If the application can be started with the command in
keelson.yaml, it can generally run on Keelson.
Keelson installs the dependencies declared in package.json during the build.
Generate package-lock.json with npm install and commit it with the source so
the build uses the locked versions. Keep command limited to starting the app.
When deployment completes, Keelson assigns a URL such as
https://my-app.keelson.run/. Only signed-in members who are allowed to use the
app can access it at that URL.
Yes. A JSON API without a user interface is a supported application form.
Bring an app from another tool
When bringing an app from another tool to Keelson, choose a deployment path based on its actual architecture, rather than the tool’s name. The same tool can generate different architectures depending on its settings and prompts.
What you can do: Deploy an app whose source code you have locally, using the path that matches its architecture
What you need: The complete source code (exported from the tool or in a Git repository), the Keelson CLI, and an AI agent (Quickstart)
Done when: The app opens at its Keelson URL and connects to the same data as in its original environment
Check these four things in the source code. Ask your AI agent to “check this app’s architecture,” and it will assess the app using the Skill’s procedure.
What to check
Where to look
Does it have server-side processing?
Dependencies and scripts in package.json. API routes, Server Actions, or Express indicate server-side processing. If it only produces vite build output, it is frontend-only
Which database does it use?
Connection code for Supabase, Firebase, or PostgreSQL. File-based SQLite (sqlite3, better-sqlite3) needs to be rewritten
How does authentication work?
Login screens and session management using Supabase Auth, Firebase Auth, Clerk, or similar services. Keelson authenticates users at the app’s entrance, so you need to decide how to handle existing authentication
Where are files stored?
External storage (Supabase Storage, S3) can stay as it is. Writes to local disk need to be rewritten
This is the simplest path. Build locally and send only the output directory. For static site deployments, the CLI uploads only the contents of assets.dir and keelson.yaml. It does not run a server-side build (npm install / npm run build).
Install dependencies and build locally. Put values that are embedded at build time, such as external service URLs and public keys, in the configuration file read during the build (.env.production for Vite, for example). Do not embed secrets in the frontend
Terminal window
npminstall
npmrunbuild# Vite outputs to dist; Next.js static export outputs to out
Add keelson.yaml. type: web is required for a static site without command
slug: my-app
type: web
runtime: node-slim
db:
mode: none
assets:
dir: dist# Locally built output directory
fallback: index.html# For SPA routing
Run keelson deploy. Use --check --json to confirm that only files under dist and keelson.yaml will be uploaded
Automatic server-side builds (npm ci and npm run build --if-present) run only for server and hybrid configurations that have a command.
Update the server to listen on 0.0.0.0 using PORT, and put the startup command in command. For Next.js, use next start after next build; for Express, use node server.js. See Framework notes for startup commands and considerations for each framework.
File-based SQLite, local disk storage, and in-app timers need to be rewritten. If you ask an AI agent to deploy the app, it automatically handles changes marked ”△ Rewrite” in the compatibility table.
You can keep using them. Set db.mode: none and pass connection details (URLs and keys) through secrets. Keelson does not restrict outbound connections.
Configurations that call Supabase directly from the browser (anon key + RLS) also work. In that case, Supabase RLS continues to protect the data.
Authentication such as Supabase Auth or Firebase Auth
This requires the most consideration. On Keelson, users must log in through Keelson before entering the app. Decide what to do with the original authentication by examining authentication and data permissions together.
Original configuration
Decision
Authentication only provides a login screen; data permissions do not depend on it
Remove the original authentication and identify users using Keelson headers such as X-Keelson-User-Id. This removes the second login
Data permissions (RLS or policies using auth.uid()) depend on users from the original authentication system
You cannot simply remove authentication. The app would lose the original user’s identity, so permission checks based on auth.uid() would no longer work (ownership policies would hide the data). Either retain the original authentication and require two logins, or move data access to the server and implement permission checks using Keelson user IDs
Per-user data is stored using user IDs from the original authentication system
You need a mapping to Keelson user IDs. Matching by email address is a practical approach
Before simplifying a two-login flow, check where permissions are enforced.
Open the app in your AI agent and ask it to “deploy this app to Keelson.” The agent assesses the app, creates keelson.yaml, declares secrets, deploys, and verifies the result. See Deploy an app for details.
After deployment, check that the app connects to the same data as in its original environment, uses Keelson login, and opens for other members.
Tool-specific backends (Lovable Cloud, Base44’s backend, etc.). Deploying source code to Keelson does not move the platform’s database, authentication, or functions with it. Keep them as external services (the tools provide guidance for hosting only the frontend elsewhere or migrating to Supabase), or rebuild them using Managed SQLite or other services
The tool’s editing features. Keelson runs the app; you edit it using your local AI agent
Languages such as Ruby, PHP, or Java, and architectures that require always-running processes. See Supported apps and constraints
Most problems that survive a green deployment come from one assumption: that the
application is a process that stays alive. It is not. This page states when your
server-side code runs, and what that rules out.
If you only remember one sentence: server-side code is guaranteed to run while
a request is in flight, and during a scheduled job you declared. There is no
third time.
An application starts when a request arrives. After a period without traffic it
stops, and the next request starts it again — so the first response after an idle
period takes longer than usual.
Server-side code therefore runs in exactly two windows:
While a request is being handled — a page view, an API call, a form
submission.
During a scheduled job you declared in crons — Keelson starts a separate
instance, runs the command, and the instance exits.
Nothing runs in between. Code written for a resident server — a scheduler that
fires at 09:00, a loop that polls an external service, work handed off after the
response has been sent — does not run at all.
Front-end code in the browser is not affected by any of this. A timer or an
auto-refresh in the page keeps working while the page is open.
None of the following produces an error. The deployment succeeds, the logs are
silent, and the screen looks correct. That is what makes this list worth reading
before you ship.
What the code does
What actually happens
What to do instead
An in-process scheduler sends a report every morning (node-cron, APScheduler, setInterval plus a clock check, threading.Timer)
Nothing happens at the scheduled time
Declare it under crons
Work is handed off after the response is returned (FastAPI BackgroundTasks, a fire-and-forget promise)
The screen reports success and the email never arrives
Finish it before responding, or record it and drain it from a crons job
The app returns “accepted” and the browser polls for a result computed in the background
The status stays “processing” forever
Record the work in the database and drain it from a crons job — and tell the user the wait can be up to one interval
A resident loop watches or synchronizes an external service
It stops when the app stops
Declare a crons entry that does one pass and exits
State is kept in a module-level variable, an in-memory session, or a local file
It disappears when the app stops
Put it in Managed SQLite
There is no queue service and no way for the app to trigger a run of itself.
Keelson does not provide an API for submitting arbitrary asynchronous tasks, and
event-driven background tasks are not currently supported. The workers:
declaration that used to exist has been retired: a keelson.yaml that contains
workers: — even an empty one — is rejected at deployment with
workers_not_supported.
Do it inside the request. Anything whose success the caller needs to know
about — validating, saving, a short external call — belongs before the response,
where its failure can be reported.
Put a time-of-day or periodic task in crons. Move the job body into its own
entrypoint that runs to completion and exits, then delete the in-process
scheduler.
crons:
- name: daily-report
schedule: "0 9 * * *"
command: "node report.js"
timeout: 300
See Scheduled Jobs for the full
configuration, plan limits, and worked examples.
Write down durable work and drain it from a crons job. For a side effect
the user should not wait for — an email, a webhook, a sync — the request handler
inserts a row into Managed SQLite and a scheduled job claims those rows, does the
work, and marks them done. Three things are not optional here:
The drain must be idempotent. A run can be stopped at its timeout after
the side effect but before the row is marked done, and the next run will see
that row again.
Count attempts and give up. There is no automatic retry and no dead-letter
queue. A row that fails forever is drained forever.
Tell the user about the delay before you build it. The row waits until the
next run, and the shortest interval any plan allows is five minutes. “We will
email you right away” is not a promise this shape can keep.
Stream output the user is waiting on. Server-sent events or chunked
responses keep the work inside the request window, so the user sees progress
instead of a spinner. The window ends when the client disconnects, so streaming
is not a substitute for durable work.
Time to start responding to a request (first response header)
120 seconds; exceeding it returns 504
Total lifetime of one request, streaming included
300 seconds
Gap between chunks of a streamed response
120 seconds
One scheduled-job run
the job’s timeout, 1–600 seconds, default 300
A 504 does not mean the work was rolled back — the handler may finish after
the deadline. Keelson never retries a POST on your behalf, so make writes that
a client might retry idempotent.
Scheduled jobs have their own rules that follow from running on a schedule
rather than on demand: a run may start late, a run that is still going when the
next occurrence arrives causes that occurrence to be skipped rather than
queued, a failed run is not retried, and a run still executing at its timeout
is stopped. Recovery is the next run.
Schedules are evaluated in the workspace time zone, which is set when the
workspace is created and cannot be changed afterwards. Confirm the effective
value before assuming your own local time.
The number of jobs per app, the shortest interval, the maximum timeout, and the
monthly execution allowance all depend on the plan — see
Plans and Limits.
In this Keelson app, server-side code is only guaranteed to run while a request
is being handled and during a declared crons run. Find every in-process
scheduler (node-cron, APScheduler, setInterval, threading.Timer) and
everything deferred until after the response is sent, and either finish that
work inside the request or move it to a crons entry in keelson.yaml.
workers: no longer exists and there is no task-submission API. Leave
front-end timers alone.
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.
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_URL and KEELSON_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.
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: none
Frameworks 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.
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"
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
conn.execute("INSERT INTO items (name) VALUES (?)", ("Example item",))
conn.commit()
The corresponding keelson.yaml enables Managed SQLite:
slug: my-app
runtime: python-slim
command: "python app.py"
db:
mode: libsql
Declare 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.
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.
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.
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.
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.
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.
Use Keelson Managed SQLite (libSQL) and declare db.mode: libsql.
Read KEELSON_DB_URL and KEELSON_DB_AUTH_TOKEN from 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 /tmp for bounded scratch data. Do not write to /data; Keelson does not
provide it as a writable platform mount.
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 SDK
Media SDK
What to store
Files 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 them
Only the app. They have no URL and are not served over HTTP
Members of the app. They are served at URLs
Overwriting
Supported (about once per second per key)
Not supported (one ID per file, write once)
Limit
10 MiB per file
50 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.
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.
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.
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
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)
Keelson can run code automatically on a schedule as well as serve an application.
You manage jobs from the same codebase and the same keelson.yaml as your web
application, making them useful for automating routine business operations.
Common use cases include:
generating a morning report and saving sales or KPI totals to a database;
synchronizing an external API with your database every hour;
sending daily Slack reminders about overdue tasks or pending approvals;
importing accepted CSV data in a nightly batch;
periodically deleting old records; and
draining work that the web application recorded for later processing.
The crons section is the only background execution mechanism available to an
application. The former workers: declaration has been retired, and Keelson does
not provide an API for submitting arbitrary asynchronous tasks.
How Apps Run explains when server-side code
is guaranteed to run and which common patterns that rules out.
Scheduled Jobs do not consume the plan quota for concurrently available web
applications. They are counted separately from web applications.
A job starts from the same codebase and image as the web application, but it runs
in a separate instance. It does not share memory or local disk with the web
application. Keelson does not provide a writable /data directory; use /tmp
only for temporary files that may be discarded when the execution ends.
Configuration
Behavior
Cron only, with no top-level command
Runs as a job-only application and does not consume the concurrent web-app quota
Web application plus cron, with a top-level command
Runs the web application and each job in separate instances; shared state belongs in a durable store
The job definitions use the same format in both configurations.
The dashboard shows whether each execution succeeded or failed.
Standard output and standard error are recorded as logs.
Usage includes runs that actually execute, whether they succeed, fail, time out,
or are triggered manually. An occurrence skipped before the container starts
does not consume the monthly execution quota.
A failed cron execution is not automatically retried. It waits for the next
scheduled occurrence.
Syntax example only; no current plan permits this interval
Not available
Cron schedules are evaluated in the workspace time zone, which is detected
automatically from the browser when the workspace is created. For example,
0 9 * * * runs at 09:00 in the workspace time zone.
A job runs repeatedly and may be interrupted. Design it so that running the
same operation again does not create duplicate data or corrupt existing state.
# Avoid an unconditional INSERT that creates a duplicate on every run.
db.execute("INSERT INTO reports (date, total) VALUES (?, ?)", (today, total))
# Prefer an UPSERT that safely replaces the row for the same date.
db.execute("""
INSERT INTO reports (date, total) VALUES (?, ?)
ON CONFLICT(date) DO UPDATE SET total = excluded.total
The schema default is 300 seconds, but the effective timeout is the lower of
that value and the plan maximum. For example, omitting timeout on Starter gives
an effective timeout of 180 seconds. An explicitly configured value must be in
the absolute 1–600 second range and at or below the plan maximum; a larger value
is rejected during deployment.
If work cannot finish in ten minutes, limit the number of records handled in
one run and let later scheduled executions continue with the remainder.
Keelson does not automatically rerun a failed job. An idempotent job can recover
naturally on its next scheduled execution. For critical jobs, also record a
failure signal or send an alert to a service such as Slack.
When a job calls an external API, account for that service’s rate limits. A
hypothetical every-minute schedule would quickly exceed many providers’
allowances, although current Keelson plans do not permit that interval.
The web process and every cron use the same Managed SQLite database. A cron can
write while the web process is writing, and different crons can overlap when
their execution times cross. The libSQL primary accepts one writer at a time, so
write contention can fail an execution with errors such as SQLITE_BUSY or
TRANSACTION_TIMEOUT.
Keep write transactions short and split large updates into bounded batches. If
several processes update the same rows, serialize those writes in application
logic or use a small, bounded retry with backoff for transient contention. Make
the operation idempotent before retrying it; Keelson does not automatically
retry a failed cron execution.
The web process and cron process do not share a local SQLite file or filesystem.
Use Managed SQLite for shared relational state and the Files SDK for durable
files. Files written to /tmp are ephemeral. Keelson does not provide a
writable /data directory.
Standard output, such as Python print calls or JavaScript console.log calls,
and standard error are captured separately. Each stream is limited to 256 KiB.
If a stream exceeds that limit, Keelson discards the excess and appends an
…[output truncated at 262144 bytes]… marker to the captured text. Output near
the end of a very verbose run, including a final exception or summary, may
therefore be absent.
Open the dashboard to inspect the execution history and captured logs for an
individual job.
Log useful progress information and record counts so a failure can be located
without reproducing the entire run.
Web applications, scheduled jobs, and durable data can work together in one
Keelson application. The database, rather than process memory or local disk, is
the handoff point between the web and cron instances.
Use the web application to enter settings and data, and let a nightly job handle
the heavier aggregation work.
slug: sales-tool
runtime: python-slim
command: "python app.py"
db:
mode: libsql
env:
PYTHONUNBUFFERED: "1"
crons:
- name: nightly-aggregate
schedule: "0 2 * * *"
command: "python aggregate.py"
timeout: 300
The 300-second timeout requires a Plus or Team plan. Starter accepts
a maximum of 180 seconds.
Declare Python dependencies in requirements.txt; Keelson installs them during
the build. The web application must listen on the PORT environment variable
injected by Keelson rather than declaring its own value.
During the day, users enter sales data through the admin interface.
At night, the job aggregates the data and updates report rows.
The next morning, the interface reads and displays those rows.
Pattern: process uploaded CSV records periodically
Record each accepted upload as a pending database row. A job can claim those
rows periodically, process them, and write back the result.
crons:
- name: process-csv
schedule: "*/30 * * * *"
command: "python process_inbox.py"
timeout: 180
This 30-minute schedule requires a Plus or Team plan. Starter jobs
must be scheduled at least 60 minutes apart.
The web application creates a pending row for the accepted CSV.
Every 30 minutes, the job processes pending rows and updates their status.
The web application displays or downloads the completed result.
Store the uploaded content with the Files SDK when the job needs the original
file. Keep its key in Managed SQLite; do not pass it through a local directory.
Output written to /tmp is not available to a later execution. A cron also runs
separately from the web instance, and Keelson does not provide a writable
/data directory. Put durable relational data in db.mode: libsql, and use an
appropriate Keelson SDK for durable file content: Files for private files or
Media for content served to app members.
A command that works locally can fail on Keelson if its API key exists only in
your local environment. Configure environment variables and secrets for the
deployed application instead of embedding credentials in source code.
A job that sends many requests every minute can be rejected by the external
service. Increase the interval, batch the work, or limit requests within a run.
Scheduled Jobs have monthly execution quotas: Starter 1,000, Plus 5,000, and
Team 15,000. After the quota is reached, remaining occurrences
for the month are skipped. Only runs that actually execute count toward usage;
skips caused by the quota, a previous active run, a disabled cron, or a stopped
application do not increase the used count. When a cron or application is
disabled, the scheduler may be removed entirely, so no skip record is guaranteed
for each tick.
Hypothetically, an every-minute schedule would have about 43,200 occurrences in
a 30-day month. No current plan permits that schedule, even for testing: the
shortest available interval is five minutes on the Team plan. Keelson
validates the plan’s minimum interval during deployment and rejects * * * * *.
This cron-only sample uses Managed SQLite. Add the following files alongside
aggregate_sales.py.
keelson.yaml:
slug: daily-sales
runtime: python-slim
db:
mode: libsql
env:
PYTHONUNBUFFERED: "1"
crons:
- name: daily-sales
schedule: "0 8 * * *"
command: "python aggregate_sales.py"
timeout: 120
requirements.txt:
libsql==0.1.11
Keelson injects KEELSON_DB_URL and KEELSON_DB_AUTH_TOKEN because the deploy
declares db.mode: libsql. The example expects the web application or a prior
migration to have created and populated sales, and to have created reports
with a unique date column.
aggregate_sales.py:
import datetime
import json
import os
import libsql
conn = libsql.connect(
database=os.environ["KEELSON_DB_URL"],
auth_token=os.environ["KEELSON_DB_AUTH_TOKEN"],
)
today = datetime.date.today().isoformat()
row = conn.execute(
"SELECT COUNT(*), SUM(amount) FROM sales WHERE date = ?", (today,)
This cron-only sample needs these files alongside sync.mjs.
keelson.yaml:
slug: catalog-sync
runtime: node-slim
db:
mode: libsql
env:
SYNC_API_URL: "https://api.example.com/items"
crons:
- name: hourly-sync
schedule: "0 * * * *"
command: "node sync.mjs"
timeout: 180
Replace the example SYNC_API_URL with the HTTPS endpoint to synchronize. If
the endpoint needs a credential, declare the credential under secrets and set
its value separately instead of committing it under env.
package.json:
{
"private": true,
"dependencies": {
"@libsql/client": "0.17.4"
}
}
Run npm install --package-lock-only after creating package.json, and commit
the generated package-lock.json so the build resolves the same dependency
version. Keelson injects the two database environment variables for
db.mode: libsql.
sync.mjs:
import { createClient } from"@libsql/client";
constdb=createClient({
url: process.env.KEELSON_DB_URL,
authToken: process.env.KEELSON_DB_AUTH_TOKEN,
});
constAPI_URL= process.env.SYNC_API_URL;
asyncfunctionsync() {
constres=awaitfetch(API_URL);
constitems=await res.json();
await db.execute(`
CREATE TABLE IF NOT EXISTS synced_items (
id TEXT PRIMARY KEY,
data TEXT,
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
for (constitemof items) {
await db.execute({
sql: `INSERT INTO synced_items (id, data) VALUES (?, ?)
For a new application, run keelson deploy --new once. The required-secret
check blocks that first deployment, but the application remains created. Store
the value without --apply, because there is no completed deployment to
redeploy yet, and then deploy the same source tree again without --new:
Use crons not only for a particular time of day, but also for work that should
be drained every few minutes. Define one short-interval schedule and let each
execution process a bounded number of pending records before exiting.
slug: mailer
runtime: python-slim
command: "python app.py"# Web: create a pending row
db:
mode: libsql# Durable state shared by web and cron
env:
PYTHONUNBUFFERED: "1"
crons:
- name: drain
schedule: "*/5 * * * *"# Team: drain every five minutes
command: "python drain.py"# Process pending rows and exit 0
timeout: 120
The five-minute schedule requires the Team plan. On Plus, use at least
15 minutes; on Starter, use at least 60 minutes.
Queue work in database rows and drain it idempotently. The web side writes
a pending row to Managed SQLite. The cron claims pending rows, performs the
work, and updates processed_at or status. Make repeating any item harmless,
because a timeout can leave it for the next run.
Do not use an in-process scheduler. APScheduler, node-cron, FastAPI
BackgroundTasks, setInterval, and similar mechanisms assume a resident
process. Keelson applications can scale to zero, so move periodic work into a
crons declaration.
Use only durable services for shared state. Put durable relational state
in Managed SQLite and private durable files in the Files SDK. A file written
to a cron’s local disk is discarded and is never visible to the web
application.
A run is stopped after at most 600 seconds. When the amount of work is variable,
set a maximum batch size and leave the rest for the next schedule. If the
previous execution is still active, the overlapping occurrence is skipped.
Every timeout also has an absolute 600-second maximum. The lower of the
plan limit and the absolute maximum applies.
A schedule more frequent than the plan’s minimum interval is rejected.
The monthly quota counts runs that actually execute, not skipped occurrences.
After it is reached, remaining occurrences are skipped without increasing the
used count until the quota resets the following month.
Keelson rejects a deployment when its crons configuration does not fit the
execution model or plan. The structured error includes a code and a hint
that you can pass to an AI agent along with the configuration.
Situation
Resolution
workers: is declared (workers_not_supported)
Delete the block and rewrite the operation as a crons entry
timeout is above 600 seconds
Reduce it to 600 or less
timeout is above the plan maximum
Reduce it to the plan limit or upgrade the plan
The schedule is more frequent than the plan permits
Remaining occurrences in that month are skipped. The quota resets the next
month. Limits are Starter 1,000, Plus 5,000, and Team 15,000 occurrences per
month.
Add a job that aggregates sales data every morning at 9:00. Define it in the
crons section of keelson.yaml.
Create a batch process that fetches an external API every 30 minutes and
synchronizes the result to Managed SQLite. This schedule requires Plus or a
higher plan.
Add a cleanup job that deletes old records every day at 3:00.
Configuring keelson.yaml
keelson.yaml tells Keelson how to build, start, and publish an application.
This page explains why the file exists, what its main fields do, and how it is
used. For the complete definition of every field, see the
keelson.yaml reference.
When you use the Keelson Agent Skill, an AI agent can generate this file for
you, so you usually do not need to write it from scratch.
keelson.yaml is an application-specific configuration file placed in the root
directory of your project.
Keelson reads it to determine:
the app identifier used in its URL
the runtime used to execute the app
how the app starts
whether Keelson should provision Managed SQLite
whether the app has scheduled jobs
whether Keelson should serve static assets
The file is not application code. It is the description of how to run that
application on Keelson. You do not need to memorize every option; start with a
small configuration and add only the features the app needs.
Applications differ in language, startup command, database requirements, and
the platform features they use. Keelson reads keelson.yaml to understand those
differences and deploy each app correctly.
Without the file, Keelson cannot decide how to handle the project. An AI agent
can inspect your code and generate the configuration, but the resulting file is
still the explicit deployment contract.
Database strategy; Managed SQLite or no platform database
libsql, none
For an application with a web process, add command. A cron-only app can omit
it and define crons instead. A static site can omit it and configure assets.
Do not put API keys, tokens, passwords, or other secret values directly in
keelson.yaml.
The file normally lives in the same repository as your source code. Add secret
values through the console and declare secret requirements using the supported
secrets configuration when needed. Use env only for non-sensitive values.
Do not set PORT in env: Keelson injects the platform-assigned PORT, and
your web server must read it when starting.
# Wrong: a secret value is committed with the app.
env:
OPENAI_API_KEY: "replace-with-a-real-key"
# Correct: configure OPENAI_API_KEY as a secret in the console.
The smallest useful web-app configuration declares slug, runtime,
command, and the required database strategy. Use db.mode: none when the app
does not need Keelson Managed SQLite.
Keelson builds the Go binary for Linux as ./app during deployment. Runtimes
come in -slim variants for general applications and -media variants that
include image and video processing libraries. Start with -slim unless the app
needs those media libraries. For every runtime, declare dependencies in the
project-root manifest (requirements.txt or pyproject.toml, package.json, or
go.mod). Keelson installs them during the image build, so command must only
start the already-built application. Each web app must listen on the injected
PORT value.
Set type: web and point assets at the built asset directory. Static sites
still declare db.mode: none because they do not use Managed SQLite.
The deploy archive normally excludes .git, .venv, __pycache__,
.pytest_cache, node_modules, dist, build, .idea, .vscode, and
.DS_Store. Keelson CLI v0.1.1 and later exempt the declared assets.dir
and its ancestors, so the dist example below is included. Exclusions still
apply inside that directory (dist/node_modules/** remains excluded), and
.git, symlinks, other non-regular files, and a file selected by
--secrets-from-env-file are never archived. Run keelson version and upgrade
before using dist or build with an older CLI.
Keelson handles sign-in and permission checks, so your app does not need authentication code. The app only reads the information added by Keelson and changes its behavior based on who the user is and what they can do.
There are three levels, depending on what you need.
What you want to do
What to use
Additional setup
Identify who is accessing the app (record the creator, show only the user’s own data)
Headers X-Keelson-User-Id / -Email / -Name
None
Create a screen that only managers can see
Header X-Keelson-User-App-Perms
None (permissions are set in the console)
Use three or more permission levels or branch by group
The proxy adds these headers. Authorization, Cookie, and headers with the same names sent by the browser are removed before reaching the app, so they cannot be spoofed. The app does not manage sessions itself.
The Identity SDK lets you read the headers with types and requires no network access.
An app has two permissions: view and manage. Assign them to groups in the console or CLI (Groups and app access). Keelson passes the result in a header.
X-Keelson-User-App-Perms
Meaning
view
Can view
view,manage
Can view and manage
The app only checks whether manage is included. Do not put group names in the code.
This level supports cases such as an app only for the accounting team or a settings screen only for managers. You can change who has manage in the console without changing the code.
me = get_current_identity(headers=request.headers)
groups = me.attributes.groups if me.attributes else []
if"accounting"in groups: ...
attributes.groups contains group keys.
Role-based system groups: Owner belongs to owners, developers, and everyone; Admin belongs to admins, developers, and everyone; Developer belongs to developers and everyone; App User belongs to everyone
Custom groups that the user belongs to and that have permission for this app. Groups that are not assigned to the app are not included
Keys do not change, so you can use them in code for checks. Non-ASCII keys such as 経理 are also supported
The return value of getCurrentIdentity also contains the user (id, email, name), their workspace role (workspace.role), and their permissions for this app (app.permissions).
When you need information about other members, such as options for an assignee field, use listMembers / list_members (with search and filtering by role or group), getUser, and listGroups.
The Identity SDK returns a dummy user when KEELSON_LOCAL_MODE=1. Change the values with KEELSON_LOCAL_USER_ID, KEELSON_LOCAL_USER_EMAIL, KEELSON_LOCAL_WORKSPACE_ROLE, and related variables. Code that reads only the headers must account for None / undefined because the headers are absent during local development.
Paths called by external systems (auth.endpoints) do not receive X-Keelson-User-Id. Authenticate these requests with a webhook signature or app token. See External integrations.
There are no restrictions on outbound traffic from an app to services such as OpenAI, Slack, Google Sheets, or an external database. Pass API keys as secrets and make the calls.
By default, all traffic coming into the app from an external source stops at the sign-in screen. You must declare callers that do not have a browser sign-in, such as a Stripe webhook or an API call from an internal system.
Declare paths that allow requests without sign-in under auth.endpoints in keelson.yaml. Paths must start with /api/webhooks/ or /api/external/.
auth:
endpoints:
- path: /api/webhooks/stripe
methods: [POST]
Requests to these paths do not receive X-Keelson-User-Id. The app must verify that the caller is legitimate.
When the sender provides a signature (Stripe, GitHub, Slack, and similar services): receive the signing secret through secrets and verify the signature
When the sender does not provide a signature: issue an app token (described below) and receive it in the X-Webhook-Secret header or in the URL /api/webhooks/<token>/...
Undeclared paths continue to require sign-in. Open only the paths required for webhooks.
The platform reserves /api/webhooks/email and /api/webhooks/email-events, so you cannot declare them as custom app webhooks. When inbound email is enabled, the platform calls /api/webhooks/email directly as described below; you do not add it to auth.endpoints.
Tokens have the format keelson_... and are shown only once, when created
--scope api corresponds to paths under /api/external/, while --scope webhook corresponds to paths under /api/webhooks/
Use --allowed-ip to restrict source IPs (CIDR, multiple values supported). This restriction applies per token, separately from workspace or app IP controls
You can configure the same settings from the app screen in the console. Use keelson apps tokens list / rotate / delete to rotate and revoke tokens
The caller adds Authorization: Bearer keelson_... and calls a path declared in auth.endpoints.
- /api/external/import# No methods specified = all methods
Before forwarding the request, the gateway checks in order that the token is valid, the scope matches, the token belongs to this app, the source IP is allowed, and the declared path and method match.
You can receive email addressed to your app through Keelson.
email:
inbound:
enabled: true
This is disabled by default. Do not configure it for apps that only send email or do not handle email.
After a successful deployment, the app receives the address <slug>@inbound.keelson.run, where <slug> is the app slug. The console shows the exact address assigned in the current environment.
Implement POST /api/webhooks/email in the app. This is a platform-reserved endpoint, so do not declare it under auth.endpoints; Keelson calls it directly when mail arrives.
The following JSON is an excerpt of one delivery, not the complete payload. Your handler should ignore additional keys.
Keelson provides the signing secret to the app in KEELSON_EMAIL_WEBHOOK_SECRET. Always verify the signature against the unmodified request body before processing the message. Choose the example for your SDK; each endpoint returns 200 after successful verification and 401 when verification fails.
Keelson is not limited to a particular framework. Any HTTP server that listens on PORT can run. However, deploying a development server can make the app slow, expose internal information on error pages, or drop requests during shutdown. This page lists production startup methods by framework.
If you ask an AI agent to deploy the app, it applies these settings automatically because the Skill contains the same information.
Bind to 0.0.0.0 on PORT. Requests cannot reach a server bound to 127.0.0.1
Do not put dependencies in command. They are installed at build time from requirements.txt, package.json, or go.mod
Shutdown is SIGTERM, then SIGKILL after 10 seconds. Finish in-flight requests within 10 seconds
1 vCPU. Use one worker
Debug mode is off by default. Debug pages may expose environment variables, including database authentication tokens
Use KEELSON_MODE to detect Keelson. The value is keelson when running on the platform. Using DEBUG for this purpose prevents the app from starting locally
Django can only run with an external database (db.mode: none plus connection details such as PostgreSQL passed as secrets). This is because Django’s ORM has no backend for Managed SQLite (libSQL).
Replace the sqlite:///app.db engine configuration with sqlalchemy-libsql-native. Models and queries do not change. This path is experimental, and an AI agent asks for confirmation before applying it.
provider = "sqlite" does not work as-is. Use @prisma/adapter-libsql pinned to the same major version as @prisma/client (verified with 6.x). An AI agent asks for confirmation before applying this change.
Keelson builds the app with go build -o /workspace/app . and starts it with ./app.
runtime: go-slim
command: "./app"
The app is built with CGO_ENABLED=0. You cannot use cgo SQLite drivers such as mattn/go-sqlite3. Use a pure Go libSQL client with Managed SQLite
Do not leave ListenAndServe running without shutdown handling. Use signal.NotifyContext and server.Shutdown to drain in-flight requests within 10 seconds
If you use GORM, replace it with database/sql plus libSQL
Set assets.dir to the build output from Vite, a Next.js static export, Astro, or a similar tool. The build runs automatically as npm run build. Even if dist/ is in .gitignore, declaring it in assets.dir includes it in the archive.
This page explains how to deploy an app you have built to Keelson. It is intended as the next step after you have tried the sample app in the Quickstart.
In Keelson, a deploy does not end when you upload your code. A deploy covers building the app, starting it, checking that it is healthy, and making it available at its app URL.
Open the app directory in an AI agent that supports Agent Skills, such as Claude Code or Codex, and ask it to deploy the project.
Example requests:
“Deploy this app to Keelson.”
“If this project does not have a keelson.yaml file, create one and then deploy it to Keelson.”
“If the app fails to start, inspect the error, fix it, and redeploy.”
The agent reads keelson.yaml and uses the CLI to perform the build and deployment. It can also inspect the project and create keelson.yaml when the file is missing.
The Agent Skill ultimately uses the Keelson CLI. You can run the same operation yourself from the project root. For the first deployment of a new app, run:
Terminal window
keelsondeploy--new
For a later deployment to the existing app, run:
Terminal window
keelsondeploy
If you belong to more than one workspace or need to choose a specific existing app, the CLI prompts for the missing target or accepts the relevant --workspace and --app options. An AI agent normally handles that selection for you.
This command creates a short-lived preview credential and makes one authenticated request. It prints the response body to standard output and the method, requested URL, and HTTP status to standard error. It does not display the credential or follow redirects. The path must begin with one /; absolute URLs and paths beginning with // are rejected so the credential cannot be sent to another host.
Add -i (or --include) to print the response protocol/status line and headers to standard error before the body is transferred. The body remains alone on standard output, so you can redirect it to a file while inspecting headers. Check both the status and Content-Type: for example, a missing JavaScript file may return 200 with Content-Type: text/html when an SPA fallback serves the page instead of the script. Authenticated GET and HEAD checks also work for the app’s /__keelson/media/... and /__keelson/assets/... paths.
The default is a read-only GET. To test an API that changes data, explicitly select POST, PUT, PATCH, or DELETE with --method. This requests a write-enabled credential that lasts 5 minutes. --data supplies the body and can use @filename to read it from a file; it is accepted only with a write method. Repeat --header 'Name: value' to add request headers. app curl does not accept --ttl.
For a multipart request, repeat --form name=value for text fields and use --form name=@path for files. With --form, the method defaults to POST when --method is omitted. The CLI infers each file’s media type from its extension; append ;type=<media-type> to override it. The CLI sets the multipart Content-Type and boundary, so a Content-Type supplied with --header cannot be combined with --form.
To use a separate HTTP client, choose a lifetime, or make multiple requests, issue the credential directly:
By default, a preview credential permits only GET and HEAD. It lasts 30 minutes, and --ttl accepts 1 to 30 minutes. --allow-writes explicitly adds POST, PUT, PATCH, and DELETE; that credential lasts 5 minutes by default and accepts a lifetime from 1 to 10 minutes. Normal output contains only the credential; --json returns the credential, app URL, and expiration time. Keelson returns the raw credential once through standard output and does not save it to disk, so keep it out of logs and reports.
If you did not create the active deployment, the command provides a browser confirmation URL. Approve the request, then repeat the command with the provided --confirmation <id>.
This check proves only that the requested operation produced that authenticated HTTP response. It does not verify every route or the app’s business logic. Write-enabled credentials work on container API routes; static sites still reject write methods because they have no write target. Read-only credentials still reject every method except GET and HEAD. Keelson cannot prevent a side effect if the app itself performs one in a GET handler, so do not use preview verification on such a route.
The CLI reports the failed stage, and an AI agent can inspect build or runtime logs to diagnose the cause. After correcting configuration or application errors, deploy again.
Start with the error message and the relevant logs. Avoid repeatedly redeploying unchanged code when the message identifies a configuration problem.
Browsers save files that control an app’s appearance, including styles and
images, and reuse them on later visits. This makes pages load faster, but it can
occasionally leave an old appearance visible just after an app update. The app’s
data may be current even while its appearance is stale.
First confirm that the deployment has completed, then try a normal reload. If
the old appearance remains, do a hard reload once:
Windows or Linux: Ctrl + Shift + R
Mac: Cmd + Shift + R
A hard reload ignores the saved appearance files and downloads them again.
Browsers that opened an app under Keelson’s previous cache policy may still need
this one-time action after the platform fix. In newly loaded sessions, unhashed
files are checked for updates during normal reloads.
Apps often need API keys, connection details, and other configuration to communicate with external services. Managing these values as environment variables lets you change configuration without hard-coding it in the application.
This page explains how to configure ordinary environment variables and sensitive secrets in Keelson.
Values in the env field are available to the running application. They are not passed into the build.
slug: my-app
runtime: node-slim
command: "npm start"
env:
NODE_ENV: "production"
PUBLIC_API_ORIGIN: "https://api.example.com"
Use this field for configuration that does not need to be secret, such as an operating mode or a public service URL.
Do not define the same key twice, and do not use YAML merge keys to construct env. Keelson rejects ambiguous definitions rather than guessing which value you intended.
Configure sensitive values from the Secrets area in the Keelson console. Keelson encrypts secret values at rest and injects them into the app at runtime.
The console lists each secret’s key, scope, and last-updated time. It does not return the stored value for display. To change a secret, enter a replacement value.
Secret names use environment-variable syntax. Choose a descriptive uppercase name such as OPENAI_API_KEY or DATABASE_URL, and make the application read that name.
A workspace secret is available to apps in that workspace. Use workspace scope for a value intentionally shared by multiple apps, such as a common service credential.
Workspace secrets are managed from the workspace’s secret settings. Changes are tracked as configuration changes for the apps that use them.
If the same key is defined in more than one place, Keelson resolves it in this order:
Priority
Source
Effect
Highest
App secret
Applies only to the selected app.
Middle
Workspace secret
Applies to apps in the workspace unless an app secret overrides it.
Lowest
keelson.yamlenv
Supplies the non-sensitive value committed with the source.
For example, you can define API_KEY as a workspace secret and then define a different API_KEY as an app secret for one app. That app receives its app-specific value.
Adding, replacing, or deleting a secret marks the app configuration as having unapplied changes. Use Apply changes in the console, or redeploy the app, to create a new revision with the updated environment.
Changing env in keelson.yaml also requires a deployment. Existing running revisions do not change in place.
Neither env values nor secrets reach the build. The build sees only the source files you upload, so a public value a frontend build needs — such as VITE_API_URL — must be written into a file that ships with your source, such as your build tool’s config file.
Be careful with frontend build variables: values embedded in browser JavaScript are public even if their names look sensitive. Never use a build-time variable to conceal a credential.
Your app reads both env values and secrets through its language’s ordinary environment-variable API.
Python:
import os
api_key = os.environ["API_KEY"]
Node.js:
constapiKey= process.env.API_KEY;
Go:
apiKey := os.Getenv("API_KEY")
Treat a missing required variable as a startup configuration error. This produces a clear deployment failure instead of an error only when a user reaches a particular feature.
Keelson injects platform-owned variables into each running app. Do not define or override them yourself.
Variable
Meaning
PORT
Port on which the web app must listen.
TZ
Workspace timezone.
KEELSON_MODE
Platform mode marker.
KEELSON_APP_ID
Internal application ID.
KEELSON_WORKSPACE_ID
Internal workspace ID.
KEELSON_TENANT_ID
Compatibility alias for KEELSON_WORKSPACE_ID (same value).
KEELSON_DEPLOY_ID
Internal ID of the current deployment.
KEELSON_APP_URL
The app’s public origin, when its host can be resolved.
KEELSON_DIRECTORY_BASE_URL
Base URL of the Directory API, when the app’s host can be resolved.
The former tenant-named variable remains accepted as a compatibility alias. No
removal date is set.
Additional variables are injected when a feature is enabled. For example, an app using managed libSQL receives its database URL and authentication token through platform-managed environment variables.
App URL — Understand the URL available after deployment.
Verify a deployment
A deployed app is behind authentication, so a normal curl request returns 401.
With keelson app curl and keelson preview, you can fetch paths from the app
while authenticated as yourself. AI agents also use this route to verify that a
deployment succeeded.
The response body goes to standard output, while the method, URL, and status go
to standard error. With -i, headers also go to standard error, so you can
redirect only the body to a file
The default method is GET. If you explicitly select POST, PUT, PATCH,
or DELETE with --method, Keelson uses a write-enabled credential that is
valid for 5 minutes
--data can only be used with a write method. Use @filename to read the data
from a file
With --form, the method becomes POST, and the CLI sets the multipart
Content-Type (you cannot override it with --header)
The path must start with /. Absolute URLs and paths starting with // are
rejected. Redirects are not followed
You can also check /__keelson/media/... and /__keelson/assets/... with
GET or HEAD
To target an app outside the current directory, use --app <slug>
Check the Content-Type as well as the status. If the SPA fallback handles a
request for a nonexistent JavaScript file, the response can be 200 with a
text/html content type.
The credential is displayed once on standard output, and Keelson does not save
it. Do not include it in logs or reports. Each user can have one credential
issued by keelson preview per app. Issuing another replaces and revokes the
previous one, even if it has not expired. keelson app curl uses a separate
internal credential and does not revoke the credential issued by preview.
If you did not create the deployment currently serving traffic, the command
shows an approval URL to open in a browser. After approval, rerun the same
command with the displayed --confirmation <id> option.
After a deployment succeeds, Keelson automatically assigns the app a standard public URL. This page explains how that URL is formed, where to find it, and how access to it is protected.
For example, an app with the slug dashboard in the acme workspace receives:
https://acme--dashboard.keelson.run
The app slug comes from the slug field in keelson.yaml. The workspace slug is managed in the workspace settings. Together they make the hostname unique across Keelson.
Keelson console — The app details page displays the URL.
Deployment result — The CLI reports the URL when deployment completes, and an AI agent can pass it back to you.
You can copy the URL and share it with team members who should have access.
Share the app URL separately with App Users. They cannot open the console, so
copy the URL from one of the places above and send it to them — otherwise they
have no way to learn it.
Keelson provides the app’s own public origin to the running process as KEELSON_APP_URL. Use it when the app must construct an absolute callback or redirect URL.
Do not derive the public origin from an internal service hostname. Read KEELSON_APP_URL, or use a relative URL when the browser already knows the current origin.
Access to an app URL is restricted to authorized users by default. All connections use HTTPS, and Keelson manages the TLS certificate; you do not need to request or renew one.
IP restrictions can add a network-level check before a request reaches the app. Authentication and IP restrictions remain in effect when users follow the standard URL.
The standard hostname contains both the workspace slug and the app slug. Changing either slug can therefore change the URL. Treat the URL as configuration rather than embedding it throughout your source code.
When another system needs a callback URL, copy the current value from the console and update that system if the app URL changes.
The Team plan lets you publish an app at your own subdomain, such as app.example.com. A workspace can register up to 10 custom domains, with one custom domain per app. Apex domains such as example.com and email domains are not supported.
Configure a custom domain in this order:
As an Owner or Admin, register the domain in the app settings in the console, or run keelson domains add <hostname> [--app <slug>]. Keelson then displays the CNAME target to use.
At your DNS provider, point the registered subdomain’s CNAME to the displayed target. The production target is custom-origin.keelson.run; always use the value shown in the console or CLI.
Check the status in the console, or run keelson domains verify <hostname> [--app <slug>], to confirm that the domain is active. Activation can take a few minutes.
Keelson automatically verifies domain ownership, issues the HTTPS certificate, and configures traffic delivery. Authentication and IP restrictions also apply through the custom domain. KEELSON_APP_URL continues to contain the standard keelson.run URL.
The app list and app page in the console show the status as a badge.
Status
Meaning
Active
The app is running. It uses one App Slot
Sleeping
The app is deployed but stopped. It starts automatically when accessed and does not use an App Slot
Published
A static site or SPA that is not manually suspended. It has no process and is served from the edge
Suspended
The app was stopped manually. It does not start or become available again when accessed. Its data, URL, and settings are retained
Starting / Deploying
The app is transitioning between states
Not deployed
The app has been created but has not been deployed yet
Error
The latest deployment or execution failed. The app page shows the cause
Deleting
The app is being deleted. It disappears from the list when deletion finishes
—
An app that only runs Scheduled Jobs. It has no running or sleeping state
Apps stop automatically when idle (sleep) and start when accessed. Only apps
accessed in the last 5 minutes count against your App Slots. If you access a
sleeping app while no slot is available, an HTTP 503 information page appears
(a slot becomes available in about 5 minutes).
To stop an app temporarily, select Settings → Suspend on the app page.
You can suspend static sites and SPAs as well as apps with a server. A suspended
app does not start in response to access, a webhook, or a cron, and its static
files are not served. Keelson’s standard URL shows the existing suspension page,
and serving from custom domains also stops. Data, URLs, and settings are not
deleted.
Select Resume in the app page header to resume the app. If you deploy a
static site or SPA while it is suspended, it remains suspended and returns to
serving only after you explicitly resume it. Stopping and resuming static-file
serving takes effect asynchronously, so the serving state may take a short time
to change after the operation.
Turn on Priority start in the app page header to reserve one App Slot at all
times. This prevents the app from being unable to start because all slots are in
use.
This setting does not keep an instance running continuously. The app still
stops when idle and starts when accessed
On Plus and higher plans, you can enable it for up to one fewer app than the
number of App Slots
The app’s standard output and standard error. You can switch between Web and cron. The tab shows the latest 200 lines, and an export can retrieve up to 5,000 lines
Access logs
Who accessed which path and when, and whether access was allowed or denied. You can filter by people, machines (app tokens), or webhooks
Deployment logs
Select View logs for a deployment on the Deploys tab. For a failed deployment, the end of the build or startup error is shown
Cron run history
The Scheduled Jobs tab shows the start time, result, duration, and logs for each run
The retention period depends on your plan. See Plans and limits.
Use keelson logs to retrieve logs from the CLI. When giving logs to an AI
agent, the most reliable approach is to paste the output of
keelson logs app <slug> --json as-is.
The Deploys tab lists previous deployments and lets you download the source
and view logs for each deployment.
Use the CLI to return to an earlier deployment.
Terminal window
keelsonrollback# Return to the previous successful deployment
keelsonrollback<deploy_id># Return to the specified deployment
A rollback does not rebuild the app. It starts a new revision with the selected
deployment’s image and settings. The current secret values are used. You cannot
roll back while a deployment is in progress.
Redeploy in the app page header starts the latest successful deployment
again with the same image. Use it to apply secret changes.
Delete an app from Settings → Danger Zone. Approval in a browser is
required. After approval, the app itself, its URL, and its execution slot are
deleted promptly. This cannot be undone.
Database backups, files stored in Files, and a configuration record (app name,
URL, cron definitions, and the names of environment variables and secrets)
are retained in quarantine for 7 days after deletion. The app cannot be
restored. Export any data you need before deleting it.
Use keelson deploy --check --json to inspect configuration and archive contents in advance, without uploading
Run keelson deploy, or ask your agent to deploy
When finished, open the URL and check the result. For authenticated API checks, see Verify a deployment
If deployment fails before traffic switches, users can continue using the old revision. However, any partially executed db.migrate changes remain (see “Change the database schema” below).
See Deploy an app for which changes take effect and when, including secrets that do not take effect until the next deployment.
Apps with a server (those with a command) can return to a previous successful deployment that has a container image.
Terminal window
keelsonrollback# Return to the previous successful deployment
keelsonrollback<deploy_id># Return to a specific deployment
Starts a new revision using the target deployment’s image and keelson.yaml settings, without rebuilding
Only completed deployments with an image can be targets. Static site / SPA deployments and deployments created by the older deployment system cannot be rollback targets
Uses current secret values
If the target deployment’s keelson.yaml contains db.migrate, rollback also runs it before switching traffic. Rollback is not necessarily an operation that leaves the DB untouched
The database is not rolled back. If newer code changed the schema, the restored code will read the newer schema. Adding columns or tables is usually compatible. If columns were removed or types changed, consider restoring the DB as well
Cannot run while a deployment is in progress
The console’s “Deployments” tab lists deployments and lets you download each deployment’s source.
To restore a static site / SPA, deploy the previous artifacts again. Check out an earlier Git commit and rebuild locally, or download the previous deployment’s source (assets.dir contents and keelson.yaml) from the “Deployments” tab and run keelson deploy directly.
Restore Managed SQLite (db.mode: libsql) data from “Restore points” on the app page in the console.
Choose a time on the restore-point timeline: a daily backup, a manual snapshot, or an arbitrary PITR time. See Plans and limits for retention counts and recovery windows by plan
You can download the data first to inspect it
Run Restore. Browser approval is required
Open the app and check the restored data
What restoration does:
All writes after the selected time are lost. This is not a partial restore of only the deleted data. Downloading before restoration lets you manually reapply changes you want to keep
The state before restoration is retained for 72 hours, during which you can undo the restore
Only one app’s DB is affected. Other apps are unaffected
Files / Media files and external databases are excluded
Take a manual snapshot before important operations (up to five per app per day).
The command in db.migrate runs on every deployment, before traffic switches.
If it fails, traffic stays on the old revision. However, partially applied changes are not automatically undone. Check migration logs (keelson logs deploy <deploy_id>) and the DB state before fixing the issue
Prefer changes that still work with the old code, such as adding columns or tables. Make migrations idempotent (for example, with IF NOT EXISTS) because they run every time
Before changes that are hard to reverse, such as dropping columns or changing types, take a manual snapshot and schedule the work during a quiet period
Requests, webhooks, and cron jobs cannot start the app. The URL shows a suspended page. Data, URL, and settings remain
Resume at any time
Delete (“Settings” → “Danger zone”)
Removes the app, URL, deployment history, and secrets. Requires browser approval
No
Export these before deletion:
DB data — Download from restore points
Files / Media files — Cannot be downloaded from the console or CLI. Implement download endpoints in the app to export them (Files and media)
Source code — Keep it in Git, or download it from each deployment in the “Deployments” tab
Secret values — Cannot be retrieved after deletion. Keep a copy if needed
After deletion, DB backups, Files, and non-sensitive configuration notes (app name, URL, cron definitions, and environment variable and secret names) are held in quarantine for seven days. This allows discretionary assistance with accidental deletion; it does not guarantee recovery.
Remember one thing first: give the error’s code and hint to your AI
agent. Keelson errors are designed so that agents can read them and make the
necessary correction. If you investigate the problem yourself, check the
following in order.
keelsondiagnose<deploy_id># Diagnose a failed deployment
keelsonlogsdeploy<deploy_id># Progress logs and details of a failed deployment
keelsonlogsapp<slug># App standard output and standard error
When a deployment fails, keelson logs deploy also shows the saved details. If
the app exits during startup, its stored startup logs preserve the traceback or
process error for that deployment. Read them with
keelson logs deploy <deploy_id> or keelson diagnose <deploy_id> before
checking the listening port and health.path. The same commands can be used
later to investigate dependency or Dockerfile build failures, unset secrets,
and configuration or migration errors. Build output from successful deployments
is not saved. If there are no progress logs or saved failure details to show,
the command only explains why no logs are available.
In the console, the app page has a failure summary under Overview,
deployment logs under View logs on Deploys, and app logs under Logs.
Where it stopped
What to inspect
Common causes
Before upload (CLI preflight check)
Command output
Missing go.mod for a Go app; a Node app with only a pnpm or yarn lockfile; pip install in command; missing db.mode; unquoted env values
Validation
code and message
Retired workers or databases fields; reserved names; plan limits
Build
Deployment logs
Dependency resolution failure; mismatch between package.json and package-lock.json; private registry; build script error
Startup
Stored startup logs in keelson logs deploy <deploy_id>
Not listening on 0.0.0.0; hard-coded PORT; exception caused by an unset secret
Health check
Stored startup logs, then app logs
The app exited during startup; / returns 5xx; startup takes more than 120 seconds
Migration
Deployment logs
SQL error in db.migrate. The previous revision continues running
Ctrl-C stops only this client from watching. The deploy is still running server-side
and may still become the active version. Use the deploy_id shown
by the CLI to check its progress:
Terminal window
keelsonstatus<deploy_id>
If you do not want the new version, wait for the deploy to finish and then
return to the previous completed deploy:
Terminal window
keelsonrollback--app<slug>
If the app has no earlier completed deploy, stop it after the deploy finishes:
Hard-coding the port — read the PORT environment variable
Depending on files that only exist locally — such as .env, a local
database file, or files covered by .gitignore. Check archive.excluded in
the output of keelson deploy --check --json to see what was excluded
Environment variables are not set — values that exist only in your local
.env are not available on Keelson. Declare them in secrets and set their
values
Using file-based SQLite — neither /data nor /tmp is persistent. Move
the data to Managed SQLite
Keelson’s Node builder uses npm, so dependency pinning requires npm’s
package-lock.json. Run npm install in the project root, commit the generated
package-lock.json, and deploy again.
You do not need to delete pnpm-lock.yaml or yarn.lock. The app can be
deployed with either file alongside package-lock.json, and the build uses
package-lock.json.
Check whether you are writing to a local file. Files in /tmp, /data, and the
app directory are lost on restart, redeployment, or an idle stop. Store the data
in a database or with the
Files / Media SDK.
Work after a response is returned, in-app timers, and threads do not run. Finish
the work synchronously within the request or move it to crons. See
Scheduled Jobs.
The cause is the browser cache. Confirm that the deployment has finished, then
perform a hard reload (Mac: Cmd + Shift + R; Windows: Ctrl + Shift + R). A
static-site redeployment takes 25–30 seconds to propagate to the edge.
This means the concurrent App Slot limit has been reached. Apps accessed in the
last 5 minutes use a slot. Wait about 5 minutes, enable
priority start for an
important app, or upgrade your plan.
The app did not start its response within 120 seconds. Shorten the operation,
send headers first by streaming, or move heavy work to crons. Server-side work
may finish even after a 504 response, so make write operations idempotent.
If you receive a platform error such as deploy.platform.error, or if the steps
above do not solve the problem, include the deploy_id (available from
keelson status --json) when you contact us.
Every app deployed to Keelson requires authentication.
Knowing an app’s URL is not enough to access it: a user must sign in and have
permission to use that app.
This page explains how authentication and login work in Keelson.
Keelson places an authentication proxy in front of every app.
Every user request passes through this proxy, which verifies the user’s identity
and permissions before forwarding the request to the app.
As a result, you do not need to implement authentication in your app.
An app gains authentication when you deploy it, regardless of the language or
framework used to build it. Even an app generated by an AI tool is not exposed
to the public without authentication when deployed as-is.
You can sign in to Keelson with either of these accounts:
Google account
Microsoft account
Only users registered as members of the workspace can sign in. A user who is
not a workspace member cannot access its apps, even if they have a valid Google
or Microsoft account.
The proxy removes incoming authentication headers and cookies before the
request reaches the app. The app receives only the identity headers added by
Keelson.
Authentication controls who can access an app. IP restrictions control
which network a request can come from.
Keelson evaluates an IP restriction after sign-in and app permission,
immediately before the request reaches the application. A request from outside
the allowed range is denied at that point. Authentication and IP restrictions
are independent controls, and you can combine them for stronger protection.
IP restrictions do not apply to static sites, SPAs, and the static files of hybrid apps.
Configuration changes take effect without a redeploy.
Keelson controls access to each app within the context of workspace membership.
The groups with view permission determine which apps a member can use, while
the groups with manage permission determine which apps a member can manage.
This page explains member management, roles, and the ways users can join a
workspace.
Keelson combines workspace membership with permissions assigned per app.
Joining the workspace is a prerequisite for using an app.
Only members of groups with view permission can use a running app.
Members of groups with manage permission can deploy and configure that app.
View and manage permissions are independent. Manage permission by itself does
not allow a member to use the running app.
A user who has not joined the workspace cannot access any of its apps.
When you create an app, view permission is assigned by default to the group
containing all members. Manage permission is assigned by default to the
developer group containing Owners, Admins, and Developers. You can change or
remove these assignments independently for each app. No role, including Owner,
bypasses the app permission check.
Can manage members other than Owners and manage settings
View and manage
Used
Developer
Not available
View and manage
Used
App User
Not available
View
Not used
The app permissions in this table are default assignments made when an app is
created. Actual access and management capabilities depend on that app’s group
assignments. No role bypasses the permission check.
Owners can add and remove members, change roles, and manage workspace settings.
They still need view permission to use an app and manage permission to manage
it. Every workspace must have at least one Owner.
Admins can manage the workspace but cannot manage members with the Owner role.
They need the corresponding app permissions to use or manage each app. This role
is appropriate for team administrators.
The Developer role is intended for members who build and deploy apps. By
default, Developers have view and manage permissions, so they can use the app
and can deploy it, change its settings, and edit its secrets. They cannot manage
members or groups, change workspace settings, or change public URLs. Removing a
Developer’s view permission for an app prevents them from using that app;
removing manage permission prevents them from performing management operations.
By default, App Users can use apps for which they have view permission, but they
are not assigned manage permission. Removing view permission prevents an App
User from using that app. An App User must be assigned manage permission to
manage an app. App Users cannot administer the workspace and do not consume a
developer seat. This role is intended for an app’s end users.
If domain auto-join is enabled for a workspace, a user from an allowed domain
becomes a member immediately after opening the workspace’s join URL. An
administrator does not need to approve the user.
A workspace with domain auto-join enabled appears in Discover. A user from an
allowed domain can request to join from Discover and becomes a member after an
administrator approves the request.
A domain can use an invitation-only, auto-join, or approval-based join policy.
In the console, you can register and remove domains. On the Team plan or higher,
you can also enable or disable the join URL to switch between auto-join and
invitation-only behavior. The current console does not provide an operation for
selecting the approval-based policy.
You can register only the domain of your own email address, and that domain must
also match the workspace creator’s email domain. The same domain can be
configured for more than one workspace.
Policy
Behavior
Plan requirement
Invitation only (default)
Only users invited by an administrator can join
None
Auto-join
Users from the domain can join through the join URL or request access through Discover
Team plan or higher
Approval-based
The join URL and Discover requests are unavailable; users join by administrator invitation
None
Members who join through auto-join receive the App User role. Auto-join is not
available for public email domains such as Gmail and Outlook.
Changing the join URL setting does not affect existing members.
No. In addition to workspace membership, the user needs view permission through
a group assigned to the app. Manage permission is a separate permission for
deployment and configuration; it does not grant use of the running app. Default
permissions can be changed independently for each app.
What is the difference between an App User and a Developer?
By default, an App User can use apps for which view permission is assigned. A
Developer has both view and manage permissions by default but cannot manage
members or workspace settings. Both roles’ default app permissions can be
changed for each app.
Can I change the role of a member who used auto-join?
This guide walks through invitations and verification using an app that only the sales team can use.
What you can do: Make an app available only to a specific group in your workspace
Required permissions / plan: Owner or Admin to invite members and create groups. Anyone with manage on the app can change its permissions. Available on all plans
Done when: A sales user can open the app, and a user outside sales sees “Access denied”
manage gives an operator permission to deploy and configure the app. That person is not necessarily a business approver. A rule such as “only managers can approve” belongs to the third layer and is enforced by the app.
Go to “Members” → “Invite,” enter the sales team’s email addresses, and select the App User role (for people who only use apps; it does not consume a Developers seat).
Recipients open the email link, log in with the account matching the invited email address, and accept the invitation. See Invite members for details.
For larger organizations, domain auto-join on Team plans and above lets employees join without an invitation.
Go to “Members” → “Groups,” create a “Sales” group, and add its members. The key (such as sales) cannot be changed after creation, so choose a stable name such as a department code.
Open the app’s “Permissions” tab. This is the key step.
New apps default to view access for everyone (all members) and manage access for developers. Adding view access for “Sales” alone still lets everyone open the app, because everyone retains view access.
Remove everyone from the view groups
Add sales to the view groups
Keep developers as the manage group (someone needs to update the app)
Members of the manage group (developers = Owner / Admin / Developer) can open the app even if they are not in a view group, because manage includes view access. You cannot hide the app from its developers. Since they can deploy it, assume they can also see its contents.
Changes take effect when saved and usually apply to users who are already logged in within two minutes.
App Users cannot open the console, so they cannot look up the app URL themselves. Copy the URL from the app page (https://<workspace>--<app>.keelson.run) and send it by chat or email. Invitation emails do not include the app URL.
Check both that authorized users can open the app and that other users cannot.
Check
Expected result
A user in the sales group opens the URL
The app appears after login
An App User outside sales opens the URL
An “Access denied” screen appears after login
Someone who is not logged in opens the URL
The Keelson login screen appears; the app is not displayed
If no one is available to test denied access, invite another account of your own (with a different email address) as an App User, leave it out of the sales group, and test with it.
If “Access scope” in the app list shows a member count and groups instead of “Everyone,” the settings have been applied.
Have they joined the workspace (accepted their invitation)? Are they in sales? Are they logged in with the invited email address? Are IP restrictions blocking them?
Users outside sales can also open it
Is everyone still in the view groups? Is the user in developers (Owner / Admin / Developer)?
A settings change has not taken effect
Changes usually apply within two minutes. Reload the browser
The steps above restrict app access to sales. Rules such as “show only my requests” or “only managers can approve” belong to the third layer. The app enforces them by reading X-Keelson-User-Id and group information.
Changing what the screen displays is not enough. Filter lists to the user’s own records, and verify on the server that users cannot retrieve or update someone else’s request by passing its ID to the API. Base these decisions on headers added by Keelson, rather than values sent by the browser. See Use identity in your app for implementation guidance.
Workspace membership is a prerequisite for using an app. After joining, the
apps a member can use are determined by the view permissions assigned to groups
for each app.
This page explains how to invite members, choose their roles, and manage
invitations.
Open the workspace member management page in the console.
Select Invite.
Enter the users’ email addresses, up to 50 at a time.
Select a role.
Create the invitations.
An invitation link is valid for seven days from the time it is created. Keelson
automatically emails each invited user a unique invitation link. When the user
opens and accepts the link, they become a workspace member.
The invitation link also appears in the console. If the email cannot be
delivered, share the corresponding link with the invited user directly.
Invitations for the Owner, Admin, and Developer roles consume developer seats
under the workspace’s plan. An unaccepted invitation that has not expired
reserves one developer seat as soon as it is created. The usage display counts
reserved seats, including pending invitations that have not expired. When an
invitation is accepted or a member’s role is changed, the limit check counts
only members who have already accepted.
You cannot invite one of these roles when the developer-seat limit has been
reached. App Users do not consume developer seats, so they can be invited
regardless of that limit.
If no developer seats are available, consider upgrading the plan or changing an
existing member who no longer needs a developer role to App User.
Only Pending invitations can be resent. Resending issues a new link and
invalidates the old link. The new link is valid for seven days from the time it
is resent.
If an invitation is expired or revoked, create a new invitation for the same
email address.
If domain auto-join is enabled for the workspace, users can join in these ways
in addition to an invitation:
Join URL — Opening the URL immediately adds the user as a member without
waiting for approval.
Discover — The user requests access from Discover and waits for an
administrator to approve the request.
Both methods are available only when the user’s email domain is an allowed
domain for the workspace. For a domain with an approval-based policy, join URLs
and Discover requests are unavailable, so an administrator must invite the
user.
No. The user needs both workspace membership and view permission assigned to a
group for each app. Members with manage permission for an app can change that
app’s permission settings.
Whether a workspace member can use an app is determined by the permissions
assigned to groups for each app. Roles (Owner / Admin / Developer / App
User) determine workspace administration permissions; roles do not determine
whether someone can use an app.
When you create an app, Keelson automatically assigns the following.
Group
Permission
everyone (all members)
View
developers (Owner / Admin / Developer)
Manage
With no changes, everyone in the workspace can use the app, and people with a
developer role can manage it. Change only the apps whose access you want to
restrict.
System groups — Determined automatically from roles. You cannot edit them.
Key
Members
owners
Owner
admins
Admin
developers
Owner / Admin / Developer
everyone
All members
Custom groups — Create these for business units such as Accounting, Sales,
or Store Staff. The key (an identifier such as accounting; Japanese characters
are also allowed) cannot be changed after creation. You can change the display
name and members. You cannot delete a group while it is assigned to an app.
In the app registry, Access shows Everyone when the app is
available to everyone. When access is restricted, it shows the number of people
and the applicable groups.
When your company has many employees, inviting them one at a time takes effort.
Registering your company’s email domain (example.co.jp) allows users with an
account on that domain to join without an invitation. This is available on the
Team plan and higher.
Users can join only when invited by an Owner or Admin
—
Auto-join
Users from an allowed domain can join immediately from the join URL. They can also submit a request from Discover
Team plan or higher
Auto-join turns on when you enable the Join URL and returns to invitation
only when you disable it. There is no screen for selecting the policy directly.
A workspace with auto-join enabled appears on the Join screen for users who
are signed in to Keelson. When a user from an allowed domain selects Request
to Join, the Owner or Admin receives the request.
Approve or reject requests under Pending Join Requests in Settings →
Security. Until a request is approved, the requester cannot access apps.
Keelson can restrict access to an app by the source IP address of each request.
With an IP allowlist, only requests from approved addresses or networks can proceed to the app. This is useful for internal apps that should be reachable only from an office, a corporate VPN, or another controlled network.
Allow access only through your office network or VPN.
Add a network boundary in addition to user authentication.
Prevent access from arbitrary networks even when someone knows the app URL.
An IP allowlist works best when your users connect through stable public egress addresses. If a network’s public address changes frequently, users may be blocked until the allowlist is updated.
When an IP restriction is active, Keelson compares the request’s source address with the effective allowlist. A request is accepted only when the address belongs to at least one allowed IP address or CIDR range.
Requests from outside every allowed range are denied before they reach the application. A valid Keelson account does not bypass the network restriction.
The IP allowlist therefore supplements authentication; it does not replace authentication or app permissions.
IP restrictions do not apply to static sites, SPAs, and the static files of hybrid apps.
Keelson stores allowed addresses in named access sources (shown as
Sources in the console) at workspace scope. Each access source can contain
one or more IP addresses or CIDR ranges. An app can either inherit the
workspace’s default access sources or select its own set of access sources.
Use the Keelson console to configure the policy:
Open Workspace Settings, then open the security settings.
Under Allowed Sources, create an access source for an office, VPN, or other
trusted network and add its IP addresses or CIDR ranges.
Add the access source to the workspace default if apps should inherit it.
To use a different policy for one app, open that app’s settings and select
the access sources for its app-specific IP restriction.
Save the configuration.
Test from both an allowed network and a network that is not allowed.
Changes take effect without a redeploy. They normally take effect within two minutes. Changes made during an incident take effect after recovery.
An app-specific policy selects from access sources already registered in the
workspace. Create or edit the address ranges in the workspace security
settings first.
Before saving a restrictive policy, confirm that you have the correct public egress address for your office or VPN. The private address shown on a laptop, such as 192.168.x.x, is usually not the address Keelson sees.
203.0.113.0/24 allows addresses in the corresponding 256-address IPv4 block. Use the narrowest range that covers the intended network.
If you are unsure which range represents your organization, ask the person who manages the office network or VPN. Do not broaden a CIDR range merely to make a failed test pass.
Open the app from an allowed office or VPN connection.
Confirm that an authorized user can sign in and reach the app.
Switch to a connection outside the allowed range, such as a separate mobile connection.
Confirm that Keelson denies the request before the app loads.
Keep an administrative console session available while testing so that you can correct an accidental lockout. Remember that a VPN may change the source address observed by Keelson.
IP restriction and user authentication are separate checks. Keelson evaluates
the IP restriction after sign-in and app permission, immediately before the
request reaches the application.
For stronger protection of an internal app:
Keep authentication enabled.
Grant app permissions only to the members who need them.
Enable an IP allowlist when access should also be limited by network location.
Passing the IP check does not grant a user access. The user must still satisfy the app’s authentication and permission requirements.
Paid plans allow an unlimited number of stored apps. Only the number that can run concurrently is limited. During the trial, separate limits apply to owned and concurrently running apps; see Limits during the trial below
Authentication, access control, and IP restrictions
Daily backups, manual snapshots (five per app per day), PITR, and backup downloads
Managed SQLite (databases isolated by app and workspace)
A 14-day free trial (once per account). You can start Starter and Plus without registering a card
The number of web apps running concurrently. A slot is automatically released about five minutes after the last access, so only apps accessed within the last five minutes consume slots.
Sleeping and suspended apps do not consume slots
Apps that only run cron jobs do not consume slots; they run in a separate allocation
Static sites do not use app slots, and there is no limit to how many you can create
Apps configured for Priority start always reserve a slot. On Plus and above, you can configure Priority start for up to one fewer app than the effective number of slots
Developer seats are used by accepted members with the Owner, Admin, or Developer role and by unaccepted invitations that have not expired. An unaccepted invitation reserves one seat when created and is included in usage. Only accepted members are counted when checking limits for accepting invitations and changing roles. App Users are not counted.
Storage is the workspace total for app artifacts, Files / Media SDK files, and snapshots. Logs are not included. Managed SQLite capacity is managed separately from this allocation and has no fixed per-plan limit.
Except for apps deployed as static sites, a trial workspace can own at most 3 apps. Undeployed apps and apps that use a server count toward this limit, and creating a 4th is rejected
Every trial workspace can run at most 3 apps at the same time, regardless of its plan. This replaces the plan’s app-slot limit during the trial and does not include extra-app add-ons, so Team drops from 4 slots to 3
Builds are limited to 7 per day
Builds are limited to 30 for the entire trial. Because these 30 builds are cumulative, the allowance does not reset when the month changes
Build time and concurrent-build limits remain at the values for your plan
When you move to a paid plan, the owned-app limit is removed and the concurrent-app limit returns to your plan’s app slots plus any extra-app add-ons
You cannot start or deploy a new app. Running apps are unaffected. If you access a sleeping app when no slot is available, an HTTP 503 information page appears. A slot is released after about five minutes
Developers
You cannot invite a member to a developer role or change a member to one. You can still invite App Users
Scheduled Jobs executions
Remaining executions for the current month are skipped. The quota resets the next month
Cron entries / minimum interval / timeout
A deployment is rejected if keelson.yaml does not comply with the plan
Builds / build time (monthly)
New builds are rejected. Both allowances reset the next month. The trial’s cumulative limit of 30 builds does not reset, so you must move to a paid plan to continue
Concurrent builds
While the limit is reached, new builds are rejected until a running build finishes
Storage
Service does not stop immediately. Support will contact you about the excess usage
Database capacity
There is no fixed per-plan limit. However, technical limits of the underlying infrastructure apply. Keelson may also restrict or stop an app if it detects a load that significantly exceeds ordinary business use or usage that affects platform stability, other customers, or Keelson’s reasonable operating costs. In an emergency, Keelson may impose restrictions without advance notice
Each account can use a 14-day free trial once. Starter and Plus can start without a card. If you do not add a card before the trial ends, it ends automatically without a charge. Team requires a card when starting the trial (cancel during the trial to avoid a charge)
If you have added a card, the trial end date is your first billing date. Subsequent charges occur monthly based on your subscription date
If an account that has already used its trial creates a second workspace, billing starts as soon as payment is completed
Your billing country determines the currency (JPY including tax for Japan, USD excluding tax elsewhere). The currency is fixed when you first subscribe; contact support to change it later
Payment by invoice or bank transfer is not supported
The number of App Users does not affect the price.
During the trial, every plan uses these limits: up to 3 stored apps (apps already deployed as static sites do not count), up to 3 concurrent apps, and up to 7 builds per day and 30 in total. See Plans and limits.
Status at the end of the trial
What happens
Card added
Your subscription converts to a paid plan and the first charge is made. The stored-app limit is removed, and concurrent-app slots return to your plan’s allowance
No card added (Starter / Plus)
The trial ends automatically without a charge. New deployments, app starts, and scheduled jobs are blocked. Apps and data are not deleted (see “Apps and data after cancellation” below)
During the trial, both upgrades and downgrades take effect immediately without a charge.
Go to Billing and usage → Change plan. The amount is shown before you confirm.
Change
Takes effect
Billing
Upgrade
Immediately
The unused portion of your current plan is deducted from the cost of the remaining period on the new plan, and the difference is charged when you change plans. The full new price applies from the next billing date
Downgrade
At the end of the current billing period
No prorated adjustment. You can keep using your current plan until then
A downgrade appears as a scheduled change. Use “Cancel scheduled change” to cancel it before it takes effect.
A warning appears if your current usage exceeds the new plan’s limits. If you switch while still over a limit:
Concurrent apps: Sleeping apps cannot start until usage falls within the limit
Developers seats: Invitations and role changes to Developer or higher are blocked until usage falls within the limit
Scheduled job runs: Runs are skipped for the rest of the month
Storage: This is a soft limit. Exceeding it does not immediately stop service; a warning appears at deployment and support will contact you. Sustained, significant overages may result in restrictions
Custom domains (Team only): Existing registrations remain, but you cannot add or re-register domains
Go to Billing and usage → Add-ons to set quantities for extra app slots (+1), extra Developers seats (+1), and extra storage (+50 GB). You cannot change add-ons during the trial; you can add them after it ends. The prorated amount is charged immediately to your saved payment method. Reductions are credited toward future invoices.
Recent invoices appear under Invoices on the Billing and usage page. Each row links to the Stripe invoice page and PDF.
Use the “Manage billing” button to open the Stripe customer portal in a new tab to change your payment method, edit billing details, or view older invoices. You can also cancel in the portal, but cancelling in the Keelson console lets you review the end date and impact first.
If a payment fails and becomes overdue, service continues as usual while Stripe retries. If payment is still outstanding after retries finish, the subscription is suspended and new deployments, app starts, and scheduled jobs are blocked. Use “Update payment method” to update your details.
Go to Billing and usage → Cancel plan, review the details, then select “Confirm cancellation”.
Your plan remains active until the end of the current billing period. Nothing stops immediately when you confirm, and the remaining period is not refunded
You can undo the cancellation with “Resume” at any time before the period ends
After the period ends, new deployments, app starts, and scheduled jobs are blocked. Cancellation itself does not delete apps or data
When a workspace’s subscription ends—after cancellation takes effect, suspension for nonpayment, or a trial ending without a card—apps can no longer start, but they are not deleted immediately.
When affected apps are identified, the Owner and Admins receive an advance deletion notice in the console and by email. The notice includes the scheduled deletion date
The grace period is 30 days from the notice. Usage restrictions remain in place, but you can sign in to the console and download database backups
Resubscribing by choosing a plan during the grace period cancels the notice and keeps your apps. This is the only way to retain them
After the grace period, apps are deleted automatically. They receive the same treatment as a user-approved deletion: only database backups, Files, and non-sensitive configuration notes are retained in isolation for 7 days. Recovery is not guaranteed
Export any data you want to keep during the grace period (see “Export your data” below).
Go to Settings → Dangerous actions → Delete this workspace. Only the Owner can do this, and confirmation requires entering the workspace name.
The workspace and all its apps are deleted, and members lose access. This cannot be undone
Each app’s database backups, Files, and non-sensitive configuration notes are retained in isolation for 7 days, but this does not guarantee recovery
Deletion signs you out
If you have an active subscription, deleting the workspace also cancels it. You do not need to cancel first. As a precaution, check the Stripe portal (open it through “Manage billing” before deletion) or invoice emails to confirm that billing has stopped
You can delete your Keelson account yourself from the account menu at the top right of the console → Account settings → Security → Delete account. This deletes your Keelson account, not the Google or Microsoft account you used to sign in. Apps and data in workspaces you belonged to also remain.
Before cancelling or deleting, export the following. The steps differ for databases and Files / Media.
Data
How to export
Managed SQLite data
Download any available point in time as a SQLite dump from Restore points on the app’s console page. This remains available during the grace period after your subscription ends
Files SDK / Media SDK files
Downloads are not available through the console or CLI. Implement authenticated download endpoints in your app for listing and retrieving files, and export them while the app is running. Apps cannot start after the subscription ends, so do this before cancelling
Source code
From Git, or download it from an individual deployment in the Deployments tab
Environment variables and secrets
Secret values are not displayed in the console. If needed, record them from their original source
Access logs and app logs
Export from the Logs tab (up to 5,000 lines for app logs)
The terms of service allow us to delete data after a period we specify following the end of the subscription (generally within 30 days). Export your data yourself before the subscription ends.
This document is the canonical definition of the supported runtimes, constraints, and requirements for deploying an app to Keelson. Use it when deciding whether an app can be deployed.
For a quickstart and step-by-step instructions, see Deploy an app.
Each application is placed in one logical region when it is created. Defined
region keys are jp-tokyo (Japan) and us-oregon (US West); Keelson shows these
display names instead of cloud-provider region names.
For a new application, select a region with keelson deploy --new --region <region>, or set the top-level region field in keelson.yaml. The CLI option
takes priority over the file, and the file takes priority over the workspace’s
default region. A region can be listed but temporarily unavailable for new
applications during rollout; an explicit unavailable selection is rejected.
The region is fixed after application creation. Deploying with another region
does not move an existing application and is rejected; create a new application
to use another region.
Keelson can run apps only on the following runtimes.
Runtime
Language
Intended use
python-slim
Python
Lightweight APIs, text processing, automation, and similar workloads
python-media
Python
Media processing; includes image and video libraries
node-slim
Node.js
Lightweight web apps, APIs, and similar workloads
node-media
Node.js
Media processing; includes image-processing libraries
go-slim
Go
Lightweight Go apps
go-media
Go
Go apps with media-processing dependencies
Select a runtime with the runtime field in keelson.yaml. If you are unsure, start with a -slim runtime and switch to -media when the app needs media-processing libraries.
Custom Dockerfiles are not supported. Keelson selects a runtime and starts the app with command. Specify the runtime and start command in keelson.yaml instead of a Dockerfile.
A deployment mode is not chosen directly. It is derived automatically from the presence of command and assets. CLI and API output exposes the raw deploy_mode label; use the following table to interpret it.
Keelson reserves only one URL namespace on an app’s host: /__keelson/*. Every other path belongs to the app. The platform does not take over common paths such as /assets, /files, /static, /uploads, or /api.
/__keelson/* is for platform internals. Keelson uses it for platform-served assets, file downloads, and internal endpoints. Do not define app routes under this path.
Do not emit a __keelson directory at the root of the served build output. A build containing this reserved path is rejected with the error code reserved_path_conflict. Rename the directory and deploy again.
No other path is reserved. Framework defaults such as /assets/*, Vite’s default output path, can be served without configuration changes.
Related rules: an auth.endpoints path cannot start with /__keelson; it must start with /api/external/ or /api/webhooks/. Reserved words also cannot be used as a slug.
The following table lists the default static paths of common frameworks. None conflicts with /__keelson/*. This is reference information: “Verified” means the behavior has been checked in this repository; “Needs verification” is a knowledge-based estimate and must be checked before it is used as evidence.
Keelson apps run on Cloud Run and may start and stop during normal operation. On shutdown, the process receives SIGTERM and has 10 seconds before SIGKILL. Use a production server and handle graceful shutdown within that window.
Flask:app.run() starts the Werkzeug development server. It may remain behind a local __main__ entry point, but it must not serve a deployment, even with debug=False. Add gunicorn to requirements.txt and use:
Keelson provides 1 vCPU, so use one worker. A graceful timeout of 9 seconds fits within the 10-second SIGTERM window. Replace app:app with the module and application object for the project.
With DEBUG=False, Django does not serve static files itself. Add whitenoise to MIDDLEWARE, configure STATIC_ROOT, and ensure the deployed artifacts include the output of collectstatic.
Keep ALLOWED_HOSTS = ["*"]; restricting it can make health checks return HTTP 400.
Replace config.wsgi:application with the WSGI module for the project.
FastAPI / uvicorn: Bind to 0.0.0.0:$PORT and leave the worker count at its default of one:
Do not use --reload. File watching consumes memory and can start the app twice. Replace main:app with the module and application object for the project.
Next.js: Start the built application, not the development server:
command: "npm run start"
Set the package script to:
"start": "next start -p $PORT"
If package.json contains "start": "next dev", fix that script rather than working around it. next dev is not a production server.
Node.js: Keelson does not set NODE_ENV automatically. Declare production mode in keelson.yaml; without it, frameworks such as Express may return stack traces:
env:
NODE_ENV: "production"
Go: Start the built binary with command: "./app". Do not leave ListenAndServe without shutdown handling: use signal.NotifyContext and server.Shutdown to stop accepting new requests and drain in-flight requests within the 10-second window.
Apply fail-fast checks for missing production configuration only when ON_KEELSON is true. Do not use DEBUG as the platform check, because that can prevent the app from starting locally.
Build succeeds, but the app exits immediately after starting
Server is not listening on 0.0.0.0
Set host="0.0.0.0" explicitly
Cannot connect to the port
Port number is hard-coded
Read the port from the PORT environment variable
Module not found
Dependency is missing from the dependency manifest or failed to install during the image build
Check the build logs and declare the dependency in requirements.txt or package.json
Native module fails to build
Required system library is missing
Switch to a -media runtime or replace the dependency
Start command not found
Entrypoint path is incorrect
Check the file name and path
Build fails with reserved_path_conflict
A __keelson directory exists at the root of the served build output
Rename the directory; only /__keelson/* is reserved
A scheduled job never fires, and no error is logged
The schedule lives inside the app process (an in-process scheduler). The app has scaled to zero by the time it is due
Declare it as a crons entry instead
The UI reports success, but the email or notification never arrives
The work was deferred until after the response was returned, so it never ran
Finish it before responding — or write it to a queue table in the database, drain it from a crons entry, and tell the user the wait is up to one interval
keelson.yaml is the deployment configuration file at the root of your
project. It defines the application runtime, startup command, environment
variables, database mode, scheduled jobs, and static assets.
Keelson reads this file during deployment and uses it to determine how to
build and run the application. For a task-oriented introduction, see
Configure an app with keelson.yaml.
Application identifier; used when the application is created
workspace
string
No
null
Default workspace for project-scoped CLI commands. Prefer the workspace slug. Omit this field when your account has only one workspace because the CLI selects it automatically. An explicit --workspace overrides this value; apps list remains unfiltered
description
string
No
null
A short description of the application, up to 300 characters
Startup command; optional for cron-only and static-only deployments
env
map
No
{}
Non-secret environment variables
db
object
Yes
—
Database and local SQLite policy
crons
list
Conditional
[]
Scheduled jobs; required if there is no command or static asset deployment
storage
object
No
{}
Compatibility block that accepts only the deprecated disk_id field
assets
object
No
null
Static asset delivery configuration
secrets
object
No
{}
Secret declarations and requirements
auth
object
No
null
Endpoints that bypass interactive Keelson authentication
email
object
No
{}
Inbound email configuration
verify
string | list
No
[]
Extra paths checked after a deployment becomes healthy
region
string
No
null
Logical placement region for a new application
The retired top-level databases and workers keys are not valid, even when
their value is null or an empty list. Use db.mode: libsql for durable
relational data and crons for time-triggered work.
The former top-level tenant field remains accepted as a compatibility alias
for workspace. No removal date is set. If both fields are present, their raw
string values must match exactly.
The application identifier. It is used as part of the application identity
and, when creating an application, contributes to its default public address.
slug: my-app
Rules:
Use lowercase letters, digits, and hyphens only: a-z, 0-9, and -.
The length must be between 1 and 63 characters.
The value must start and end with a letter or digit.
Consecutive hyphens are not accepted.
Reserved slugs are admin, api, assets, auth, console, health,
static, and www.
public_slug is a deprecated compatibility field. It is ignored because the
public URL is derived from the application host.
An optional one- or two-sentence explanation of who the application is for
and what it does.
description: "An internal dashboard for reviewing support requests."
The value is trimmed and may contain at most 300 characters. An empty value is
treated as unset. During deployment, the description is used only when the
application does not already have a description, so a description edited in
the Console is not silently overwritten.
The only accepted value is web. It enables static asset delivery through
assets and permits a deployment without a top-level command when static
assets are present.
Constraints:
type: web cannot be combined with crons.
Omitting type is appropriate for ordinary application and cron-only
deployments.
Keelson installs dependencies automatically during the image build. If the
application uses external libraries, put the appropriate dependency manifest
at the project root. Keep command limited to starting the application. If
command includes pip install,
npm install, or go build, the CLI pre-deploy check stops the deployment
with command_installs_dependencies.
Runtime
Detected files
Build step
python-*
requirements.txt
python -m pip install --user -r requirements.txt
python-*
pyproject.toml with [project] or [build-system]
python -m pip install --user .
node-*
package-lock.json / package.json
npm ci when a lockfile is present, or npm install, followed by npm run build --if-present
go-*
go.mod / go.sum
go mod download, then go build -o /workspace/app . (start it with command: "./app")
Build constraints:
An installable Python pyproject.toml must be accompanied by a recognized
lockfile: requirements.txt, poetry.lock, uv.lock, or Pipfile.lock.
Node builds use npm and require package-lock.json. If only
pnpm-lock.yaml or yarn.lock is present, the pre-deploy check stops with
lockfile_unsupported. Those files may remain alongside
package-lock.json.
A Go module that declares third-party dependencies must include go.sum.
A standard-library-only module does not require it.
Dependencies must be available from public registries. Private registries,
git+ssh dependencies, and authenticated installs fail.
Defines non-secret environment variables as key-value pairs. All values are
strings.
PORT is set by Keelson at runtime; do not declare it in env.
Always quote environment variable values. An unquoted value is rejected
with env_value_not_string, and deployment does not start. Numbers and
booleans must also be quoted.
env:
NODE_ENV: "production"
DEBUG: "false"
LOG_LEVEL: "info"
Quoting is required because the CLI and API parse the file using YAML 1.2 and
YAML 1.1 implementations, respectively. Those versions can assign different
types to the same unquoted scalar. Quoting guarantees that both paths deploy
the same string.
An unquoted key must start with a letter or underscore and contain only
letters, digits, and underscores. Ordinary names such as NODE_ENV and
DB_POOL can therefore be written without quotes.
The following words cannot be used as unquoted keys because YAML 1.1 may
interpret them as booleans or null:
Quote a key to use a hyphen, a reserved YAML word, or another otherwise
unsupported spelling.
env:
NODE_ENV: "production"
"MY-VAR": "x"
"yes": "x"
Keys beginning with KEELSON_, case-insensitively, are reserved for the
platform and cannot be set in env. Duplicate env declarations, duplicate
keys inside env, and YAML merge keys (<<) are also rejected.
Use secrets to declare sensitive settings. Do not commit secret values to
keelson.yaml.
Keelson provisions an isolated managed database and injects KEELSON_DB_URL and KEELSON_DB_AUTH_TOKEN
none
Keelson does not manage a database; use this for database-free apps or an external database
Managed SQLite with mode: libsql requires no separate account or connection
configuration. It supports scale-to-zero and is the recommended option for
durable relational data.
For PostgreSQL, MySQL, or an independently managed libSQL service, select
mode: none and configure connection details as secrets. Keelson does not
inject credentials for an external database.
There is no mode that makes a SQLite file under /data durable. Applications
that need durable SQLite semantics must use a libSQL client with
mode: libsql.
Defines scheduled jobs. Keelson starts each job according to its schedule in a
separate instance from the web service.
crons:
- name: cleanup
schedule: "0 3 * * *"
command: "python cleanup.py"
timeout: 60
enabled: true
Field
Type
Required
Default
Description
name
string
Yes
—
Unique job name, 1–63 lowercase letters, digits, or hyphens
schedule
string
Yes
—
Valid five-field cron expression
command
string | list
Yes
—
Command to execute
timeout
integer
No
300
Timeout in seconds, from 1 through 600
enabled
boolean
No
true
Whether Keelson schedules the job
Rules:
A configuration may contain at most 10 jobs. The plan may impose a lower
limit.
Job names must be unique.
type: web and crons cannot be combined.
A job runs in a different instance from the web service, so local files such
as /data are not shared.
If the previous run is still active, Keelson skips the overlapping run.
Schedules are evaluated in the workspace time zone. This setting is detected
automatically from the browser when the workspace is created.
The platform-wide timeout ceiling is 600 seconds. A plan-specific ceiling
may be lower.
Plan limits for Starter, Plus, and Team are, respectively: 3, 5, and 10 jobs;
minimum intervals of 60, 15, and 5 minutes; and timeout limits of 3, 5, and
10 minutes.
workers for background workers or periodic drains has been retired. Any
top-level workers key is rejected with workers_not_supported, including an
empty list or null value.
Represent time-triggered background work with crons. A drain-style process
can run on a short schedule, process the pending items, and exit successfully.
Configures static asset delivery for static sites, SPAs, and hybrid
applications that combine static files with a backend API.
assets:
dir: dist
fallback: index.html
api: /api
Field
Type
Required
Default
Description
dir
string
Yes
—
Asset directory relative to the project root
static_dir
string
No
null
Deprecated alias of dir; if both are present, they must match
fallback
string
Conditional
null
Relative fallback file for an SPA, such as index.html
api
string
No
null
Path prefix forwarded to the backend, such as /api
When api is set, requests under that path go to the backend application and
all other requests are served as static assets.
When the CLI creates the deploy archive, it normally excludes paths containing
any of these names: .git, .venv, __pycache__, .pytest_cache,
node_modules, dist, build, .idea, .vscode, and .DS_Store. The
declared assets.dir and its ancestor directories are exempt, so build output
such as dist is included. Built-in exclusions still apply below assets.dir:
for example, dist/index.html is included but dist/node_modules/** is not.
.git is always excluded, as are symlinks, other non-regular files, and a file
selected by --secrets-from-env-file, regardless of its name. The CLI stores
only that project-relative path, never secret values, in the automatically
excluded .keelson-config/previous-secrets-files.json file.
Env-style files and directories are also excluded, including inside
assets.dir. A final path element is treated as an env name when it starts
with .env or ends with .env, case-insensitively. Examples include .env,
.env.production, .envrc, and .secrets.env. This rule applies whether or
not --secrets-from-env-file is used.
To exclude project-specific content, create .keelsonignore in the project
root. It accepts one gitignore-like pattern per line, ignores blank lines and
# comments, and supports /, trailing /, *, ?, **, [abc], [a-z],
and leading ! negation. Matching is case-sensitive and the last matching rule
wins. Negation can deliberately re-include an env file such as
!.env.production, but cannot re-include .git, built-in exclusions, runtime
build exclusions, .keelsonignore itself, or a file selected by
--secrets-from-env-file. Invalid patterns produce a warning and are ignored.
For a static app with assets and no string- or array-form command, the
archive includes only files under assets.dir and keelson.yaml by default;
ancestor directories remain traversable so the CLI can reach the asset
directory. Files outside that tree are not uploaded. A leading ! rule can
deliberately restore an outside file. To restore a directory and its contents,
use both !keep/ and !keep/**. Container and hybrid apps still archive the
source needed for their builds.
Avoid assets.dir: . and equivalent paths such as ./: the project root is
then the public asset directory, so every otherwise archived file can be
served. Keelson warns before deployment but does not reject this configuration.
Run keelson deploy --check --json and review its archive.excluded,
archive.excluded_env_files, archive.secret_like_files,
archive.embedded_credentials, archive.unscanned_files, and
archive.unscanned_binary_count values before upload. Embedded credential
entries expose only the path and kind, never the value. Preview and app-token
matches block upload; webhook-signing-only matches warn and continue. Files over
16 MiB and files classified as binary are not content-scanned, and excluded
files are outside the scan. secret_like_files warns about secret-like names
and about an existing file selected by an earlier --secrets-from-env-file
deploy when it is not selected for exclusion this time. Such files remain
included, including non-env names such as prod-secrets.txt. The
assets.dir exemption requires Keelson CLI v0.1.1 or later; check with
keelson version and upgrade an older release before deploying assets from
dist or build.
JavaScript and CSS files with a clear content hash in the filename, such as
index-B7hK2mQ1.js, are cached for one year because the content at that URL is
expected never to change. Keelson recognizes a final 8–64 character segment
made of ASCII letters, digits, and underscores, containing both letters and
digits, as a hash. HTML, unhashed files such
as style.css and app.js, images, and all other assets are checked for updates
on every visit. When a file has not changed, its ETag makes this a lightweight
validation request.
You can redeploy an unhashed file under the same name. For hashed files, make
sure your build tool generates a new filename whenever the content changes.
Rules:
dir cannot contain a .. path component.
fallback must be a relative path under assets.dir.
api must begin with /, cannot be /, and cannot be under /__keelson.
api is valid only when a top-level command provides a backend.
A hybrid deployment with both assets and command requires fallback.
Each required entry must contain exactly one of any_of or all_of. The
selected list must be non-empty and may reference only names declared in
items. An optional message supplies remediation text.
Configures application endpoints that are intended for external systems, such
as webhook senders, and therefore do not use the normal interactive Keelson
login flow.
auth:
endpoints:
- /api/webhooks/payments
- path: /api/external/status
methods:
- GET
auth.endpoints is required when auth is present and must be a non-empty
list. Each entry is either a path string or an object with path and optional
methods.
Endpoint paths must be unique and start with /api/external/ or
/api/webhooks/. Paths under /__keelson are reserved. If methods is
provided, it must be a non-empty list.
Adds explicit paths to the post-deployment verification probe. The default
probe always checks /; these paths are additional entry points that Keelson
should fetch after the deployment becomes healthy.
verify:
- /dashboard
- /settings
Use this for routes or assets that cannot be discovered from the default entry
page, such as a route loaded only through a dynamic import. Omit it when the
default probe is sufficient.
A declared path must return 2xx or 3xx. A 404, 401, 403, 405, or any other
response outside those ranges fails verification with
verification_declared_path_unreachable, for both container/hybrid apps and
static/SPA apps.
Write only paths that do not require the app’s own user login. Verification
requests identify the candidate revision but do not carry an app user identity,
so an app-level 401 or 403 fails the deploy.
/ remains a tolerant default even if it also appears in verify; an API-only
app may legitimately return 404 there.
This differs from health.path: the health probe goes directly to
the candidate revision and accepts 404 as proof that the process started.
verify checks whether a declared public path is actually servable, so 404 does
not pass.
Optionally selects the placement region when creating a new application.
region: us-oregon
Use a Keelson logical region key, not a cloud-provider region identifier:
Key
Display name
jp-tokyo
Japan
us-oregon
US West
For a new application, selection priority is the CLI --region option, then
region in keelson.yaml, then the workspace default. An unknown or
not-yet-available explicit selection is rejected instead of falling back to
another region.
An application’s region is fixed when the application is created. A later
deploy with a different region is rejected and cannot move the application;
create a new application to use another region.
Deployment mode is derived from the presence of command, assets, and
fallback; it is not selected directly. CLI and API output such as
keelson status --json returns the raw deploy_mode label.
The web application writes pending rows to Managed SQLite. The cron processes
those rows from a separate instance, so both processes share state through
db.mode: libsql, not through the local filesystem.
Remove the install or build step; dependencies are installed automatically during the image build
A dependency manifest is missing (missing_dependency_manifest)
For Go, put go.mod at the project root. For Python or Node.js, add the appropriate manifest if the app uses external libraries; a standard-library-only app does not need one
Environment variables
This page lists the environment variables that Keelson sets in your app container. Do not add any of them to env in keelson.yaml. Names beginning with KEELSON_ are reserved by the platform and cannot be used in env or secrets.
Target app. If omitted, resolved from keelson.yaml in the current directory
--workspace <slug|name|id>
Target workspace. Required when you belong to multiple workspaces
--json
Output the result as JSON. Use this from scripts and agents
--quiet
Output a single line
--timeout / --retry
Timeout and retry count for API calls
--no-browser
Display the URL without opening a browser
When a command fails with --json, it returns {"error":{"code","message","hint","retryable"}} on standard output. Follow hint. If retryable is false, repeating the same command will not change the result.
The former --tenant flag and keelson tenants list command remain accepted
as compatibility aliases. No removal date is set.
Create an app and deploy it for the first time. Register values from an env file as secrets and exclude that file from the archive
keelson deploy --check [--json]
Validate the configuration and display archive contents (excluded files and files that may contain secret values) without uploading
keelson deploy --ndjson --yes
For scripts. Stream progress as one JSON object per line and finish with {"result":"success"|"failed"}
keelson status [deploy_id]
Show app or deploy status
keelson diagnose [deploy_id]
Diagnose a failed deploy (with an ID), including stored startup logs when startup failed, or the running app (without an ID)
keelson deploys list
Show deploy history
keelson rollback [deploy_id]
Return to the previous successful deploy, or the specified deploy, without building
The NDJSON stream may include lines such as
{"stage":"health_check","status":"progress",…} while a stage is running.
Decide success or failure only from the line that contains result.
Show deploy progress logs and stored failure details, including startup logs when the app exited during startup. Build output for a successful deploy is not retained, so the command explains when no logs are available
Deleting an app, running SQL, and restoring a snapshot cannot be completed with the CLI alone. The command displays an approval URL and exits with confirmation_required. After you approve the operation in a browser, it runs on the server. You do not need to run the command again. In an interactive terminal, use --wait to wait for completion. Approval expires after 15 minutes.
Errors are returned with a code and a hint that explains how to resolve them. You can pass them directly to an AI agent for resolution. When a workspace-named code replaces an older tenant-named code, error.aliases contains the old code for compatibility; no removal date is set. This page groups a human-readable list by where each error occurs.
These findings are reported before upload and can also be checked with
keelson deploy --check. Errors stop the deployment; warnings do not. A
missing dependency manifest is an error for Go, but only a warning for Python
and Node.js.
Code
Cause
Resolution
missing_dependency_manifest
No dependency manifest for the selected runtime
For Go, add go.mod to the project root. For Python or Node.js, add a manifest if the app uses external libraries; no dependency manifest is required for a standard-library-only app
command_installs_dependencies
command contains pip install / npm install / go build
Remove it and leave only the start command
runtime_command_mismatch
The languages of runtime and command do not match (for example, node with python-slim)
Correct either one
assets_dir_empty
assets.dir is missing or empty, or the path uses the wrong letter case
Run the build and check the path
assets_fallback_missing
The assets.fallback file is not in assets.dir
Check the build output
db_wiring_fail (warning)
db.mode does not match the database connection in the code
Switch to a libSQL client that reads KEELSON_DB_URL
Invalid YAML, a db.mode other than libsql / none, db.migrate used with none, env beginning with KEELSON_, a path under /__keelson, an unknown runtime, more than 10 crons, or another configuration error
Correct the issue described in message
config_missing_field
A required field such as slug, runtime, or db is missing
Add it
env_value_not_string
An env value or key is not quoted, a key is duplicated, or a merge key is used
Quote every value and key
description_too_long
description is longer than 300 characters
Shorten it
workers_not_supported
workers: is present
Remove it and use crons
DB_DATABASES_REMOVED
databases: is present
Remove it. Use db.mode: libsql for persistent data
The limit for build count, build time, concurrent builds, or consecutive failures was exceeded
The message states when the restriction clears. For consecutive failures, fix the cause and wait until the stated time. The trial build allowance is cumulative and does not reset by waiting; change plans in that case
saved_config_unsupported (409)
The keelson.yaml for a redeploy or rollback does not conform to the current schema
Correct it and perform a normal deploy
region_not_available (422)
The selected region is not accepting new applications
Create the app in Japan (jp-tokyo) or wait for the region to open
region_unknown (422)
The selected value is not a Keelson logical region key
Use jp-tokyo (Japan) or us-oregon (US West), not a cloud-provider region name
region_immutable (422)
A deploy tried to change an existing application’s placement region
Remove --region and deploy again, or create a separate application
Use --workspace <slug>. Search with keelson workspaces list --query
app_not_found
The app was not found
Check the slug. For a new app, use --new
app_manage_required
The app exists, but you do not have manage permission
Ask a current manager to add you to a group with manage permission. Workspace OWNERs and ADMINs can add themselves with keelson groups members add <group> <email>
app_deleting
The app is being deleted
Wait for completion
forbidden
You do not have permission. CONSOLE_ONLY_OPERATION identifies an operation limited to the console
Check the role and app permissions
confirmation_required
The operation requires browser approval
Open the displayed URL and approve it. Do not run the command again
Log in with an account registered in the workspace
”Access denied” (403)
The user is not a member, is blocked, or lacks app view permission
Ask an administrator to register the member or grant permission
”This application is only available from approved networks” (403)
Rejected by IP controls. The page shows the source IP
Ask an administrator to add an access source for that IP
”The app cannot start right now because no app slot is available.” (503)
Limit on the number of apps that can run concurrently
Wait about 5 minutes. Consider priority startup or a plan change
”Starting the app”
Starting from sleep
Wait a few seconds; the page reloads automatically
”This application has been suspended” (503)
The app is suspended
An administrator must resume it
”App not found” (404)
The URL is wrong or the app was deleted
Check the URL
410 Gone
The app URL changed. The old URL is reserved for 30 days
Direct users to the new URL
504
The app did not respond within 120 seconds
Shorten the operation or move it to a cron
501
WebSocket is not supported
Use SSE or polling
When the same situation occurs through an API or XHR request, the response has the same status as JSON and the x-keelson-error header contains the reason, such as ip-restricted, app-view-denied, capacity-full, or app-suspended.
The detailed reason is returned in X-Keelson-Auth-Error; for compatibility, the JSON detail remains machine-forbidden or webhook-forbidden for IP allowlist denials.
machine-forbidden and webhook-forbidden intentionally group a missing app, a token belonging to another app, and a route that disappeared so callers cannot use the response to determine whether an app exists.
X-Keelson-Auth-Error
Cause
invalid-app-token
The token is invalid or revoked
machine-scope-denied
The token scope (api / webhook) does not match the path
machine-endpoint-denied
The path or method is not declared in auth.endpoints
machine-forbidden / webhook-forbidden
The app is missing, the token belongs to another app, or the route disappeared
machine-ip-not-allowed
The request came from outside the app token’s allowed IP ranges
invalid-webhook-secret
The Webhook secret does not match
webhook-scope-denied
The token does not have the webhook scope
webhook-ip-not-allowed
The request came from outside the Webhook token’s allowed IP ranges
webhook-endpoint-blocked
The path is reserved by the platform, such as /api/webhooks/email
CLI usage telemetry
Keelson uses CLI usage telemetry to understand which workflows succeed, find reliability problems, and improve the CLI. It is enabled by default.
Exit code and, when available, the published failure code
Command duration
Names of flags used, but not their values
Whether the command ran in an interactive terminal (TTY)
CLI version
Type of agent that launched the CLI; it is recorded as unknown when it cannot be determined
For list commands, only whether the result contained zero, one, or multiple items
A unique identifier for each event, used to discard duplicates
The date and time the command ran
A session identifier. This is a random value created on the device and is replaced after 30 minutes without a recorded command. Stored event rows do not contain account, workspace, or app identifiers. Events are sent using your signed-in credentials, and submission counts are tracked per account, so the transmission itself is not anonymous.
To disable telemetry through the environment instead, set:
Terminal window
KEELSON_NO_TELEMETRY=1
The environment variable takes precedence over the saved preference. While it is set, keelson telemetry on cannot enable telemetry. Disabling telemetry deletes pending event records, and you can check the current state at any time with keelson telemetry status.
Disabling telemetry stops event recording and sending and removes the X-Keelson-Client header. The regular User-Agent header still includes the CLI version as keelson-cli/<version>.
Immediately before the first telemetry send, the CLI displays a one-line notice when it is running in an interactive terminal. It does not display this notice in non-interactive automation.
keelson feedback is a separate mechanism for sharing feedback you provide. It sends only when explicitly run and feedback sharing is enabled. Feedback sharing is disabled by default. Turning CLI usage telemetry on or off does not change the feedback-sharing setting.