# Keelson documentation (full text) Keelson is a platform for running internal apps built with AI tools behind login and permissions. This file concatenates every English documentation page. Index: https://keelson.dev/llms.txt Deployability is decided by the Keelson Deploy Spec: https://keelson.dev/docs/reference/deploy-spec.txt # Introduction --- # Documentation guide Explore what Keelson supports, find the right guide, and ask AI questions about your app URL: https://keelson.dev/docs/introduction/what-is-keelson/ Language: en --- Keelson helps your team use internal apps built with AI tools or other development tools. Along with deploying and running your app, it handles login and lets you choose which members can access it. To get started, create an account or sign in to the [Keelson console](https://console.keelson.dev/sign-in). ## Ask AI about Keelson You can ask questions about Keelson in ChatGPT, Claude Code, or another AI tool you use. Click **Copy** in the bar at the top of the page and send the URL along with your question. Describe how your app is built to ask whether it can run on Keelson and what changes it needs. Try the following prompt, replacing the app description with your own: ```text https://keelson.dev/llms.txt Read the documentation at this URL and follow relevant links as needed. I want to deploy an internal app built with Python and SQLite to Keelson. Tell me whether it is supported and what I need to prepare. ``` ## Choose your starting point - [Try a sample app](https://keelson.dev/docs/introduction/quickstart/) — Deploy a sample and check that only signed-in users can open it. Takes about 15 minutes. - [Bring an existing app](https://keelson.dev/docs/building-apps/bring-your-app/) — Deploy an app built with Lovable, Bolt, v0, or your own development tools. - [Solve a problem](https://keelson.dev/docs/deploy/troubleshooting/) — Find help when a deployment fails, someone cannot open an app, or saved data disappears. ## Supported apps and use cases Keelson supports Python, Node.js, and Go web apps, prebuilt static sites, and scheduled jobs that run at set times or intervals. It is designed for internal business apps used by a few people to a few hundred people. The number of app users does not affect the price. Apps that require an always-running server, WebSockets, or a GPU are not supported. See [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) for frameworks and detailed requirements. Existing apps that meet these requirements can be deployed after adjusting their configuration and any code that depends on the runtime environment. You do not need to rewrite the entire app for Keelson. You can also keep using existing external databases and services. See [Bring an existing app](https://keelson.dev/docs/building-apps/bring-your-app/) for the preparation steps. For considerations when moving an app to another environment, see the [FAQ](https://keelson.dev/docs/introduction/faq/#can-i-run-my-app-outside-keelson). Visit the [main website](https://keelson.dev/) for an overview and pricing. ## Guides by role ### Build and deploy apps **Start here** - [Quickstart](https://keelson.dev/docs/introduction/quickstart/) — try deploying a sample app - [Core concepts](https://keelson.dev/docs/introduction/core-concepts/) — learn the terminology and what Keelson provides versus what your app implements - [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) — check whether your app can run on Keelson **Refer to these as needed** - [When apps run](https://keelson.dev/docs/building-apps/how-apps-run/) — understand when apps start and stop, and how to run tasks - [Databases](https://keelson.dev/docs/building-apps/persistent-storage/) and [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) — choose where to store data and files - [Use the signed-in user's identity](https://keelson.dev/docs/building-apps/using-identity/) — record who created data or tailor what each user sees - [Deploy an app](https://keelson.dev/docs/deploy/deploy/) and [Update and restore](https://keelson.dev/docs/deploy/update-and-restore/) — deploy updates and return to an earlier version - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) and [CLI commands](https://keelson.dev/docs/reference/cli/) — look up settings and commands ### Manage members and access - [Invite members](https://keelson.dev/docs/workspace/invite-members/) — invite people to your workspace - [Members and roles](https://keelson.dev/docs/access-control/members-and-permissions/) — understand roles and what each role can do - [Share an app with your team](https://keelson.dev/docs/access-control/share-with-team/) — restrict an app to a specific team - [Groups and app permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) — configure who can use each app - [IP restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) — choose which IP addresses can access your apps - [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) and [Billing and subscriptions](https://keelson.dev/docs/workspace/billing/) — review plan limits and manage your subscription ### Develop and deploy with an AI agent The [Keelson Deploy Spec](https://keelson.dev/docs/reference/deploy-spec/) describes the requirements and configuration for deploying an app. Give your AI agent the following URL and ask: “Read this specification and review my app's code, then suggest the changes needed to deploy it to Keelson.” ```text https://keelson.dev/docs/reference/deploy-spec.txt ``` The Agent Skill installed by `keelson install-agent` includes the Deploy Spec and instructions for adapting Python, Node.js, and Go code. To give an AI tool all the documentation in one file, use [`/llms-full.txt`](https://keelson.dev/llms-full.txt). To get an individual page as Markdown, replace the trailing `/` in its URL with `.md` (for example, `/docs/introduction/quickstart/` → `/docs/introduction/quickstart.md`). ## Frequently asked questions See the [FAQ](https://keelson.dev/docs/introduction/faq/) for common questions before getting started. --- # Quickstart Deploy a sample app and check that only signed-in users can open it URL: https://keelson.dev/docs/introduction/quickstart/ Language: en --- Deploy a sample app to Keelson and check that only signed-in users can open it. - **Time:** about 15 minutes, including the build. - **What you need:** a Keelson account and a terminal for running commands. The sample does not need API keys for any external services. - **What success looks like:** you see a greeting after signing in, while a private browser window where you are not signed in shows the login page. This guide uses Claude Code, Codex, or Cursor to deploy the app. You can also deploy directly with the CLI: skip step 3 and run the CLI command in step 4. If you do not have a Keelson account, create one in the [Keelson console](https://console.keelson.dev/sign-in). ## 1. Install the CLI and sign in The Keelson CLI lets you sign in and deploy from your terminal. Select your operating system and run the install command. **macOS / Linux** ```bash curl -fsSL https://keelson.dev/install.sh | sh ```
If the keelson command is not found The installer does not change `PATH`, the list of places your terminal looks for commands. Add this line to `~/.zshrc` or `~/.bashrc`, then reopen your terminal: ```bash export PATH="$HOME/.keelson/bin:$PATH" ```
**Windows (PowerShell)** Run this in Windows PowerShell 5.1 or later, included with Windows 10, Windows 11, and Windows Server 2016 or later. ```powershell irm https://keelson.dev/install.ps1 | iex ``` After installation, make `keelson` available in your current PowerShell session: ```powershell $env:Path = "$env:USERPROFILE\.keelson\bin;$env:Path" ```
Make the command available in future PowerShell sessions The installer does not change `PATH`. Open **Edit environment variables for your account**, add `%USERPROFILE%\.keelson\bin` to your user `Path`, then reopen PowerShell.
Next, run the login command. Your browser opens so you can sign in with your Keelson account. ```bash keelson login ``` ## 2. Download the sample app 1. [Download the sample app (ZIP)](https://keelson.dev/downloads/keelson-hello-en.zip). 2. Extract the ZIP file. 3. Open the extracted `keelson-hello` folder in Claude Code, Codex, or Cursor. If you are not using an AI agent, open a terminal in that folder.
See the sample code ```python # main.py from html import escape from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse app = FastAPI() @app.get("/", response_class=HTMLResponse) def index(request: Request): email = escape(request.headers.get("X-Keelson-User-Email") or "Guest") return f"""

Hello, {email}

This app is running on Keelson.

""" ``` ```text # requirements.txt fastapi uvicorn ``` ```yaml # keelson.yaml slug: hello runtime: python-slim command: "uvicorn main:app --host 0.0.0.0 --port $PORT" db: mode: none ``` The app reads the signed-in user's email address from a header supplied by Keelson and displays it. It contains no authentication code.
## 3. Install the Agent Skill The Agent Skill gives your AI agent Keelson configuration and deployment instructions. It comes with the Keelson CLI. Open a terminal in the extracted `keelson-hello` folder, select your agent, and run the command. **Claude Code** ```bash keelson install-agent claude-code --global ``` With `--global`, the skill is available in your other projects too. Without it, the skill is installed only in the current project. **Codex** ```bash keelson install-agent codex ``` Installs the skill in the current project. **Cursor** ```bash keelson install-agent cursor ``` Installs the skill in the current project.
Install the Skill from the public repository Choose exactly one installation method: `keelson install-agent`, the Claude Code plugin, or `npx skills add`. Combining methods can create two copies of the same Skill or cause one installation to overwrite the other. In Claude Code, add the marketplace and install the plugin: ```text /plugin marketplace add keelsonhq/skills /plugin install keelson@keelson-skills ``` For other agents, use: ```bash npx skills add keelsonhq/skills ``` These methods install only the Skill. You still need to install the CLI and sign in as described in step 1. A Skill installed as a Claude Code plugin is not updated by `keelson login`; it follows Claude Code plugin updates instead.
See [CLI commands](https://keelson.dev/docs/reference/cli/#setup) for installation locations and how to update the skill. ## 4. Ask the agent to deploy Send this request in your agent's chat: > Deploy this app to Keelson. The agent starts the deployment. The first deployment takes a few minutes. When it starts, you receive a deployment ID and an app URL in this format: ```text https://--hello.keelson.run ``` If you are not using an AI agent, run this in the `keelson-hello` folder: ```bash keelson deploy --new ``` The URL is issued before deployment finishes, so the app may still be building or starting when it appears. Before opening the URL, check the status. Replace `` with the ID shown when deployment started. ```bash keelson status ``` For example, for ID `d1`, run `keelson status d1`. A result of `status: completed` means the deployment process has finished. Continue to step 5 to check that you can actually open the app. Statuses such as `queued`, `building_image`, or `starting` mean the deployment is still in progress. Wait a little and run the same command again. `failed` or `rolled_back` means it did not succeed: check the reason and suggested next action. See [Troubleshooting](https://keelson.dev/docs/deploy/troubleshooting/) for help. ## 5. Check the app and login protection 1. Open the app URL in your browser. Sign in to Keelson if prompted. Check that the page shows “Hello, …” and the text below it. 2. Open the same URL in a **private or incognito window** where you are not signed in to Keelson. You should see the Keelson login page instead of the app. Seeing “Guest” does not mean you are signed out. The sample displays “Guest” when your login method does not provide an email address to the app. You now have an app at its own URL that only signed-in users can open. It also appears in the [Keelson console](https://console.keelson.dev). ## Next steps - **Share with a colleague** — invite them and check that they can open the app using their own account. See [Share an app with your team](https://keelson.dev/docs/access-control/share-with-team/). - **Change the page and deploy again** — edit the text in `main.py` and ask the agent to deploy again. See [Update and restore](https://keelson.dev/docs/deploy/update-and-restore/) for applying changes and returning to an earlier version. - **Deploy your own app** — check [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/), then prepare with [Bring an existing app](https://keelson.dev/docs/building-apps/bring-your-app/). - **Understand how it works** — read [Core concepts](https://keelson.dev/docs/introduction/core-concepts/) for workspaces, permissions, and the responsibilities of Keelson and your app. --- # Core concepts How workspaces, apps, and groups fit together, and what Keelson handles for your app URL: https://keelson.dev/docs/introduction/core-concepts/ Language: en --- Keelson keeps your internal apps and members together in a workspace. You do not need to rebuild login or member management every time you add an app. This page introduces the key terms and explains which features Keelson provides and which your app implements. ## Key terms | Term | Meaning | |---|---| | Workspace | A place to manage members and apps together | | App | A business tool you deploy and use, such as a sales tracker or daily report app | | Member | A person who belongs to a workspace | | Group | A collection of members, such as a department or team, that you can assign access to together | | Deploy | Send your app's code and configuration to Keelson so it can run there | For example, you could create a sales report app and an expense request app in the same workspace, then share the sales report with the sales team and the expense app with all employees. ## What happens when someone opens an app Deploying a web app or static site gives it a dedicated HTTPS URL. Keelson obtains and renews the certificate. ```text https://--.keelson.run ``` Knowing the URL is not enough to open a regular app page. Keelson checks that the visitor is signed in and has permission to view the app, then passes authorized requests to it. ```text Browser ↓ Keelson: checks login, app access, and IP restrictions ↓ App: displays pages and runs business logic ``` Your app does not need to implement a login page or manage Keelson login sessions. It can identify the visitor using the user information Keelson supplies. Requests that do not use browser login, such as webhooks from external services, need separate configuration. See [External integrations](https://keelson.dev/docs/building-apps/external-integrations/). ## Manage members and access in one place ### Share login across apps in a workspace After signing in once, a user can open any app they have access to in the same workspace. They do not need to sign in to Keelson separately for each app. If an app also uses its own external authentication, that service may require a separate login. ### Assign app permissions to groups You manage members and groups in the workspace, then assign permissions to groups for each app. | Permission | What it allows | |---|---| | View (`view`) | Open and use the app | | Manage (`manage`) | Deploy the app and change its settings and secrets | View permission lets someone access and use an app. Whether they can create or edit data inside it depends on the app's code. New apps start with these permissions: | Group | Members | App permission | |---|---|---| | `everyone` | All workspace members | View | | `developers` | Members with the Owner, Admin, or Developer role | Manage, which includes view | To restrict an app to a specific team, see [Share an app with your team](https://keelson.dev/docs/access-control/share-with-team/). **App management permission is separate from business approval authority.** Rules such as “only a manager can approve a request” or “only the assigned person can edit a case” belong in your app, using user and group information. ## What Keelson provides and what your app implements For storage, you can use Managed SQLite, a dedicated database managed by Keelson, and the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/), libraries for storing and retrieving files, images, and videos. Responsibilities are divided as follows: | Feature | Keelson provides | Your app implements | |---|---|---| | Pages and business logic | An app URL and runtime environment | Forms, reports, approval requests, and other features | | Login and permissions | Login, member management, and checks on who can open an app | Business rules, such as showing only the current user's data | | Data storage | Managed SQLite and the Files / Media SDK | Code that connects to storage and reads and writes data | | API keys and other secrets | Storage and delivery of secrets to the app | Code that uses the supplied keys to connect to external services | | Scheduled jobs | Starting work at configured times or intervals | Code that performs one run of the task and exits | | Operations | Logs, app listings, and Managed SQLite backup and restore | Logging business outcomes and errors | You can describe the features you want to an AI agent and ask it to create the app code and Keelson configuration. ## Shared features you can use ### Manage API keys as secrets Set shared API keys at the workspace level and pass them to apps as environment variables. An app can override a shared value with its own. You do not need to put secret values in source code or `keelson.yaml`. See [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/). ### Give your app its own database Add this to `keelson.yaml` to provision a dedicated Managed SQLite database: ```yaml db: mode: libsql ``` Keelson supplies the connection information. Your app implements the code to connect and read or write data. Managed SQLite includes backup and restore. Files / Media files are not included in those backups. See [Databases](https://keelson.dev/docs/building-apps/persistent-storage/) and [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) to choose the right storage. ### Run tasks at set times or intervals Configure daily reports or recurring data synchronization under `crons` in `keelson.yaml`. Jobs can run even when no one is visiting the web app. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/). ### Check app activity from the app list The console shows who created each app, who can use it, and when it was last used or deployed. Other members can understand how an app is managed without relying on its creator. ## What to know about startup and storage - **Web apps stop after a period without requests.** They start again on the next request, so the first response after stopping may take longer. Work that continues after a response has been sent is not guaranteed to finish. - **Files on the app server are temporary.** They can be lost on restart or redeployment. Store data you need to keep in Managed SQLite or through the Files / Media SDK. See [When apps run](https://keelson.dev/docs/building-apps/how-apps-run/) and [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) for implementation details. ## Read next - [Quickstart](https://keelson.dev/docs/introduction/quickstart/) - [Use the signed-in user's identity](https://keelson.dev/docs/building-apps/using-identity/) - [Members and roles](https://keelson.dev/docs/access-control/members-and-permissions/) - [Deploy an app](https://keelson.dev/docs/deploy/deploy/) --- # FAQ Common questions about supported apps, pricing, access, and data before getting started URL: https://keelson.dev/docs/introduction/faq/ Language: en --- Find answers to common questions about supported apps, pricing, access, and data. Follow the links in each answer for detailed instructions. ## Supported apps and runtime ### Can I deploy an app built with Lovable, Bolt, or v0? It depends on how the app is built. Start by asking the AI tool that created it, or an AI agent that can inspect its code. You can copy this prompt: ```text https://keelson.dev/llms.txt Read the documentation at this URL and follow relevant links as needed. Check whether this app can be deployed to Keelson. Review its code, configuration, database, authentication, and external services, then explain the changes and preparation needed. Ask me if you need more information to make that assessment. ``` Prebuilt HTML, CSS, and JavaScript can be deployed as a static site. Apps with server-side processing, such as Next.js apps, can be deployed as web apps. Build static sites locally and configure their external service URLs before deploying. You can keep databases and storage that accept external connections, but data and authentication tied to the app-building tool do not move automatically. See [Bring an existing app](https://keelson.dev/docs/building-apps/bring-your-app/). ### Can I run my app outside Keelson? Yes. With the configuration and code changes needed for the destination, your app can run locally, on another hosting service, or on your own server. Provide the runtime, such as Python or Node.js, install dependencies, and configure startup and database connections for the new environment. If you still need the login, access controls, and scheduled jobs that Keelson handled, you must provide them in the new environment. Adapt code that reads Keelson's signed-in user information to your new authentication method. Code using the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) also needs changes for the destination's storage service, along with migration of the files. ### Does Streamlit work? Not currently. Streamlit requires WebSockets, which do not fit Keelson's execution model. Gradio is under evaluation. See [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/). ### Can I use Django? Yes, as long as it connects to an external database such as PostgreSQL. Managed SQLite (libSQL) has no backend for the Django ORM, so Django with Managed SQLite is not supported. Set `db.mode: none`, pass the connection details as secrets, and start it with gunicorn. See [Framework notes](https://keelson.dev/docs/building-apps/frameworks/#django). ### Is my app always running? No. Web apps stop after a period without requests and start again on the next request. The first response after stopping may therefore take longer. Use scheduled jobs for work that must run at a set time. Work that continues after a response has been sent is not guaranteed to finish. See [When apps run](https://keelson.dev/docs/building-apps/how-apps-run/). ## Pricing and limits ### When does billing start? For first-time users, billing starts when the 14-day free trial ends and the workspace moves to a paid subscription. - **Starter / Plus:** you can try these without a payment card. If you do not add a card before the trial ends, you are not charged. If a card is on file, your paid subscription starts and the first charge occurs when the trial ends. - **Team:** a payment card is required to start the trial. Cancel during the trial to avoid a charge. If you continue, the first charge occurs when the trial ends. If the trial ends without a paid subscription, new deployments, app startup, and scheduled jobs stop being available. See [What happens after a trial or subscription ends?](#what-happens-after-a-trial-or-subscription-ends) below for how data is handled. If your account has already used its trial and you create another workspace, billing starts when payment completes. See [Billing and subscriptions](https://keelson.dev/docs/workspace/billing/#when-billing-starts). ### Which plan fits two developers and 50 app users? Plus or higher. Developer seats are for members who build and manage apps. Two people doing that work need two seats. The 50 people using the apps can join as App Users, and their number does not affect the price. Owner, Admin, and Developer roles count toward developer seats. Also check that the concurrent app limit fits your needs: Starter allows 1, Plus 2, and Team 4. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/). ### If I keep 10 apps but only use a few at a time, which limit matters? Paid subscriptions have no limit on the number of apps stored in a workspace. The app-count limit applies to apps used concurrently. Apps accessed within the last five minutes occupy a concurrent app slot. When requests stop, an app becomes sleeping and its slot is released. During the trial, a workspace can also hold at most three apps. Apps already deployed as static sites do not count toward those three. See [Concurrent apps](https://keelson.dev/docs/workspace/plans-and-limits/#apps-running-at-once). ### What do users see when all concurrent app slots are occupied? Someone opening a sleeping app sees a page asking them to wait for a slot. An unreserved slot becomes available about five minutes after the last request to the app using it. It stays occupied while that app continues to receive requests. [Priority startup](https://keelson.dev/docs/deploy/app-status-and-logs/#priority-startup) can reserve a slot for an important app (Plus and higher on paid subscriptions; all plans during the trial). ### Are external API costs, such as OpenAI usage, included? No. External API usage is billed separately under your agreement with that provider. Store the API key as a secret for your app to use. See [External integrations](https://keelson.dev/docs/building-apps/external-integrations/). ## Access and data ### Can anyone with the URL open my app? No. A visitor must be signed in as a workspace member and have permission to view that app. Visitors who are not signed in see the login page. To share with someone outside your company, invite them as a member; App Users are unlimited. See [Authentication and login](https://keelson.dev/docs/access-control/auth-and-login/). ### Where do apps and databases run? Are databases backed up? Apps and their Managed SQLite databases are hosted in the region chosen when the app is created. The default is Japan. Japan is currently the only region available. US West is in preparation; selecting it before release is rejected. Managed SQLite includes backup and restore on every plan. Files stored through the Files / Media SDK and external databases are not included in these backups. Daily backups, manual backups, point-in-time recovery (PITR), and downloads are available. See [Backup and restore](https://keelson.dev/docs/building-apps/persistent-storage/#backups-and-recovery). ### Will updating an app erase saved data? A normal redeployment does not erase data in Managed SQLite or files stored through the [Files SDK](https://keelson.dev/docs/building-apps/sdk/) or [Media SDK](https://keelson.dev/docs/building-apps/sdk/). Local files on the app server, however, are lost on restart or redeployment. If a database migration or your app code changes or deletes data, those changes take effect. See [Databases](https://keelson.dev/docs/building-apps/persistent-storage/) for storage options and [Update and restore](https://keelson.dev/docs/deploy/update-and-restore/) for recovery. ### What happens after a trial or subscription ends? If a trial ends without a paid subscription, or a canceled subscription reaches the end of its term, new deployments, app startup, and scheduled jobs become unavailable. Apps and data are not deleted immediately. Owners and Admins receive deletion notices in the console and by email for affected apps. From the time the notice is recorded, the grace period is three days for a workspace that has never paid, or 30 days for a workspace with a payment history. Check the notice for the scheduled deletion date. Starting or resuming a paid subscription within the grace period cancels the notice and keeps the apps and data. Without a subscription, affected apps are automatically deleted after the grace period. Database backups and Files files are then retained in isolation from normal use for seven days, but recovery is not guaranteed. Managed SQLite backups can still be downloaded during the grace period. Export Files / Media files before the subscription ends. See [Apps and data after cancellation](https://keelson.dev/docs/workspace/billing/#apps-and-data-after-your-subscription-ends). ### What happens if I downgrade? A downgrade during a paid subscription takes effect at the end of the current billing period. You keep the current plan until then. Plan changes during a trial take effect immediately. Changing plans does not itself delete apps or data. Usage may be restricted if you exceed the new plan's limits. For example, sleeping apps cannot start while all concurrent app slots are occupied, and you cannot add members who build or manage apps while over the developer seat limit. See [Change plans](https://keelson.dev/docs/workspace/billing/#change-your-plan) for other limits. # Building Apps --- # Supported apps and constraints Apps you can deploy to Keelson, supported runtimes, and execution environment constraints URL: https://keelson.dev/docs/building-apps/supported-app-types/ Language: en --- Keelson supports Python, Node.js, and Go web apps, static sites, and scheduled jobs. Depending on your framework and storage choices, you may need to change code or configuration. Check the table below. You do not need a Dockerfile. Keelson does not use one for its build, even if your app includes one. ## App compatibility | App | Status | Notes | |---|---|---| | Python web apps (FastAPI, Flask, and others) | Supported | Start on the port specified by `PORT`. Use gunicorn for Flask | | Node.js web apps (Express, Hono, Next.js, and others) | Supported | Start on the port specified by `PORT`. Use `next start` for Next.js | | Go web apps (net/http, Gin, and others) | Supported | Keelson builds the app and starts it with `./app` | | Prebuilt static sites / SPAs (React, Vue, Svelte, and others) | Supported | Point `assets` to the build output | | Static frontend + API server (hybrid) | Supported | Set the API path with `assets.api` | | Scheduled jobs only | Supported | Set `crons` alongside the shared required fields; omit the web server `command` | | Connections to external PostgreSQL / MySQL / libSQL | Supported | Set `db.mode: none`. Supply connection details through secrets | | Calls to external APIs (OpenAI, Slack, Google Sheets, and others) | Supported | Outbound connections are unrestricted, except that the SMTP ports (TCP 25, 465, 587, 2525) are always blocked | | Persistent data in SQLite files (`sqlite3`, `better-sqlite3`, and others) | Changes required | Switch the connection to a libSQL client and set `db.mode: libsql`. SQL usually stays as it is (Managed SQLite is SQLite-compatible), but adapt to client API differences (async in Node.js, `row_factory` and others in Python). Local development in Python / Node.js keeps working with `file:local.db` | | Data that must persist stored in local files | Changes required | Use Managed SQLite or the [Files SDK / Media SDK](https://keelson.dev/docs/building-apps/sdk/). Local data written during execution does not survive restarts | | In-app timers or background threads | Changes required | Move work that runs at set times to scheduled jobs, and enqueue work that continues after a request as a [background task](https://keelson.dev/docs/building-apps/background-tasks/) ([How apps run](https://keelson.dev/docs/building-apps/how-apps-run/)) | | Django | External database only | Connect to an external database such as PostgreSQL with `db.mode: none` and start with gunicorn. Managed SQLite (libSQL) has no backend for the Django ORM, so Django with Managed SQLite is not supported ([Framework notes](https://keelson.dev/docs/building-apps/frameworks/#django)) | | Streamlit | Unsupported | It requires WebSocket and currently does not work ([Framework notes](https://keelson.dev/docs/building-apps/frameworks/#streamlit--gradio)) | | Gradio | Under evaluation | Version 4 and later are being evaluated. Operation is not currently guaranteed | | Ruby / PHP / Java / Rust / .NET / Elixir | Unsupported | Only Python, Node.js, and Go runtimes are available | | Adding apt packages through a Dockerfile | Unsupported | No root access. A `-media` runtime may cover image or video requirements | | Apps requiring continuously running processes, daemons, or WebSocket servers | Unsupported | Incompatible with the execution model, which stops idle apps | | Running your own Redis / Kafka / database server | Unsupported | Connecting to an external service is allowed | | GPU workloads | Unsupported | | Apps marked “Changes required” may work after code or configuration changes. Ask your AI agent to review the app and propose the necessary changes. You can store temporary data that can be regenerated in local files. Temporary SQLite use requires a `db.local_sqlite` declaration. See the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#db) for requirements. For apps built with other tools, such as Lovable, Bolt, or v0, see [Bring an app from another tool](https://keelson.dev/docs/building-apps/bring-your-app/). > **Tip: Ask AI to check compatibility** > > Give your AI agent the following URL and ask it to check whether your app can be deployed to Keelson. It can review your code and propose any required code or configuration changes. > > ``` > https://keelson.dev/docs/reference/deploy-spec.txt > ``` ## Runtimes Select a runtime with `runtime` in `keelson.yaml`. Start with `-slim` if you are unsure, and switch to `-media` if you need image, video, or PDF processing libraries such as Pillow, sharp, or ffmpeg. | Runtime | Language version | Build behavior | |---|---|---| | `python-slim` / `python-media` | Python 3.12 | Automatically installs dependencies from `requirements.txt` or `pyproject.toml` | | `node-slim` / `node-media` | Node.js 24 | Automatically installs dependencies from `package.json` and `package-lock.json`. Runs `npm run build` if defined | | `go-slim` / `go-media` | Go 1.26 | Builds from `go.mod` using `go build` and starts as `./app` | The Go version is the toolchain used for the build. The build steps in this table apply to apps with server-side processing and scheduled jobs. Dependencies are installed at build time. Put only the startup command in `command`. - **Node.js:** Keelson uses npm for builds on the platform. Projects with only `pnpm-lock.yaml` or `yarn.lock` are unsupported; provide `package-lock.json`. Static sites built locally with pnpm or Yarn can still be served. - **Go:** Builds use `CGO_ENABLED=0`. Libraries that depend on cgo cannot be used. See the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#dependency-installation) for dependency file requirements and build constraints. ## Execution environment constraints | Item | Behavior | |---|---| | OS / CPU | Linux / x86_64 | | Permissions and system packages | Runs as a non-root user. `sudo` and `apt-get` are unavailable | | Web server startup | Listen on `0.0.0.0` and the port specified by `PORT` | | Data storage | Local files are not persistent. Use Managed SQLite or the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) for data you need to keep | | Processing | Request handling, background tasks, and scheduled jobs are supported. See [How apps run](https://keelson.dev/docs/building-apps/how-apps-run/) | See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for execution time, app count, and other limits. ## Minimal app This Python (FastAPI) example can be deployed with three files: `keelson.yaml`, `requirements.txt`, and `app.py`. ```yaml # keelson.yaml slug: my-app runtime: python-slim command: "python app.py" db: mode: none ``` ```text # requirements.txt fastapi uvicorn ``` ```python # app.py import os from fastapi import FastAPI import uvicorn app = FastAPI() @app.get("/") def index(): return {"message": "Hello from Keelson"} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=int(os.environ["PORT"])) ``` Your AI agent normally writes `keelson.yaml`, so you do not need to create it yourself. ## Next steps - [Configure keelson.yaml](https://keelson.dev/docs/building-apps/keelson-yaml/) — Introduction to the configuration file - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) — Files / Media SDK - [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) - [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) - [Keelson Deploy Spec](https://keelson.dev/docs/reference/deploy-spec/) — Authoritative specification for AI agents --- # How apps run When web apps start and stop, and when to use request handling, background tasks, or scheduled jobs URL: https://keelson.dev/docs/building-apps/how-apps-run/ Language: en --- This page explains how apps with server-side processing run. Static sites do not need an app server to start. In a hybrid app that combines a static frontend with an API server, the startup and shutdown behavior below applies to the API server. Keelson starts your app server when it receives traffic and stops it after a period without traffic. The next request starts it again. Handle page loads and API calls within requests, enqueue work that continues after a request as a background task, and use Keelson scheduled jobs for work such as daily reports. Work that assumes the app is always running is not supported. ## When web apps start and stop When a request arrives while an app is stopped, Keelson starts the app. The first request after a shutdown may therefore take longer than usual. After a restart, variables held in memory and data written to local files during execution are lost. Save data you need to keep in Managed SQLite or with the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/). See [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) and [Files and media](https://keelson.dev/docs/building-apps/files-and-media/). ## Work that runs within a request Complete all work needed for a request, including saving data and calling external APIs, before returning the response. Background work that continues after the response is sent is not guaranteed to finish. If your app uses FastAPI `BackgroundTasks` or starts asynchronous work without waiting for it, you need to change how that work runs: `await` short work before returning the response, and enqueue work that must continue after the response as a [background task](https://keelson.dev/docs/building-apps/background-tasks/). You can use streaming to return results incrementally, such as an AI-generated answer. Streaming does not provide a way to keep processing after the connection closes. ## Work that continues after a request Declare work such as heavy external API calls, PDF or image generation, or anything that exceeds the request time limit under `tasks` in `keelson.yaml`, and enqueue it from your app with the SDK. Keelson runs the task's command once in an execution environment separate from the web app and retries it automatically if it fails. A task may run more than once, so write it so that running it twice does no harm. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) for setup instructions. ## Work that runs on a schedule Configure daily reports or periodic data synchronization in the `crons` section of `keelson.yaml`. These jobs run on schedule even when nobody is using the web app. Each job starts in an execution environment separate from the web app and exits when its command finishes. Save data shared with the web app in a database or another persistent store. Timers and schedulers inside the app do not run while the app is stopped. Use Keelson scheduled jobs for recurring work. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) for setup instructions. ## Choose how to run your work | What you want to do | How to implement it | |---|---| | Save a submitted form | Save during the request, then return the result | | Display an AI answer as it is generated | Stream the response during the request | | Create a report every morning | Configure a scheduled job | | Sync with an external service periodically | Run one synchronization pass per scheduled job | | Accept heavy work and run it after the response | Enqueue it as a background task | | Collect work and process it later in batches (a wait is acceptable) | Save pending work in the database and process it with a scheduled job | Saving work for later introduces a delay until the next job runs. Use a background task for work that should start right away. In either case, you need to handle duplicate processing and failures. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) and [Job design rules](https://keelson.dev/docs/building-apps/scheduled-jobs/#design-rules). Keelson does not support continuously running workers. Enqueue work that continues after a request as a background task. ## Work in the browser The behavior above applies to server-side processing. These restrictions do not apply to browser-side timers or automatic page updates. ## Execution time limits HTTP requests, background tasks, and scheduled jobs each have a time limit. Split long-running work into units that finish within the applicable limit. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/#http-time-limits), [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/), and [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/). ## Related pages - [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) - [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) - [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) --- # Bring an app from another tool Have an AI agent review your existing app, prepare its data and login settings, and deploy it URL: https://keelson.dev/docs/building-apps/bring-your-app/ Language: en --- You can bring apps built with Lovable, Bolt, v0, or local development tools to Keelson if you have their source code. Compatibility and the changes needed for deployment depend on the app's language, structure, and data storage. Start by asking an AI agent to review the code, then review the required changes before deploying. **Deploying the code does not move data or user accounts from external services to Keelson.** ## 1. Get the source code Export the code from the tool you used to build the app, or clone its Git repository locally. Open the app's folder in your AI agent. If you have not installed the Keelson CLI and Agent Skill or signed in to Keelson, follow steps 1 and 3 of the [quickstart](https://keelson.dev/docs/introduction/quickstart/). When installing the Skill per project, run the installation in the folder of the app you are bringing over, rather than the sample app folder. ## 2. Ask your agent to review the app Use a request like this: > I want to deploy this app to Keelson. Start at https://keelson.dev/llms.txt and read the linked supported apps and constraints page and Keelson Deploy Spec. Then review the code and explain whether it can be deployed and what needs to change. Check the database, login, file storage, and recurring tasks. Explain any changes that affect existing data or users' access before making them. Your agent will mainly check these areas: | What to check | Why | |---|---| | Language and framework | To confirm support and determine how to build and start the app | | Data storage | To decide whether to keep the external database or switch to Managed SQLite | | Login and permissions | To ensure existing users can still access their own data | | File storage | To check whether stored files would be lost on restart | | Timers and background work | To decide whether each piece of work moves to scheduled jobs (runs at set times) or [background tasks](https://keelson.dev/docs/building-apps/background-tasks/) (continues after a request) | See [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) for languages and frameworks, and [How apps run](https://keelson.dev/docs/building-apps/how-apps-run/) for execution behavior. ## 3. Review data and login ### If the app uses an external database or storage service You can keep using external services that the app can connect to after deployment. In that case, you do not need to move the data. For example, an app using an external database should set `db.mode: none` and supply connection details. Pass server-side API keys and passwords through [secrets](https://keelson.dev/docs/deploy/environment-variables/). Configure service URLs and public keys used by the browser according to the service's instructions. Do not embed secret values in code sent to the browser. If the browser calls a service such as Supabase directly, that service's access controls still apply. Protecting the app with Keelson login does not automatically replace permissions on external data. ### If the app already has a login system Opening a Keelson app requires Keelson login. If you keep the original login, users will need to sign in to both. Whether you can remove the original login depends on how the app uses it to identify users and control data access. | Role of the original login | What to do | |---|---| | Only restricts who can open the app | Consider replacing it with Keelson login and app permissions | | Restricts which data each user can see | Keep the original authentication, or change authorization to use Keelson user information | | Stores the original service's user IDs in data | Decide how to map existing users to Keelson users | For example, if Supabase row-level security (RLS) uses `auth.uid()`, removing login alone breaks the ability to identify whose data is being accessed. Check how data access works before deleting the login screen. ### If the app stores data in local files Data written to local files while the app runs does not survive restarts or redeployments. Choose persistent storage based on what you need to keep. | What you need to keep | Keelson storage | |---|---| | Customer records, projects, requests, and other data to search and update | [Managed SQLite](https://keelson.dev/docs/building-apps/persistent-storage/) | | Internal configuration files or records of processed data | [Files SDK](https://keelson.dev/docs/building-apps/files-and-media/#files-sdk) | | Images displayed in the app, PDFs, and attachments users open | [Media SDK](https://keelson.dev/docs/building-apps/files-and-media/#media-sdk) | If you change storage, ask your agent to explain how to move existing data as well as how to change the code. ## 4. Deploy Before deploying, install the Keelson CLI and Agent Skill and sign in to Keelson. If needed, follow steps 1 and 3 of the [quickstart](https://keelson.dev/docs/introduction/quickstart/). After reviewing the changes, ask your agent: > Make the changes we reviewed, create keelson.yaml, and deploy this app to Keelson. The agent creates a configuration for your app's structure. The main options are below; you do not need to write the configuration yourself. | App structure | How it is deployed | |---|---| | Static site / SPA (prebuilt HTML, CSS, and JavaScript) | Serve built files through `assets`. This includes apps that call external APIs from the browser | | Web app with server-side processing (Next.js, Express, FastAPI, and others) | Start the web server with `command` | | Static frontend combined with an API server | Set `assets` and `command`, and use `assets.api` to specify the API path (hybrid structure) | See [Configure keelson.yaml](https://keelson.dev/docs/building-apps/keelson-yaml/) to understand the settings and [Deploy your app](https://keelson.dev/docs/deploy/deploy/) for deployment steps. ### Building static sites and hybrid apps Build static sites locally before deploying. Keelson receives the contents of `assets.dir` and `keelson.yaml`; it does not run `npm install` or `npm run build` for a static site. For a hybrid app, build the frontend locally and place the output in `assets.dir` before deploying. Keelson builds the server-side code, but serves the uploaded files as `assets`. Set service URLs and public keys that are read at build time before building. Setting server environment variables after deployment does not update an already-built frontend. Ask your agent to handle those settings together with the build and deployment. ## 5. Check the deployed app - The app opens at its Keelson URL and the required actions work. - Existing data appears, and new or updated data is saved. - Images and attachments can be viewed or opened. - Users can follow the intended login flow, including any original authentication you kept. - Other members can use the app, and cannot see data they should not have access to. ## What does not move with the code Databases, authentication, file storage, and server-side functions provided by the original tool do not move when you deploy source code. Keep connections to services accessible from outside the tool. Otherwise, you will need to migrate data or rewrite the affected functionality. The original tool's editing interface does not move to Keelson either. After deployment, change the code with your local AI agent or other tools, then redeploy. ## Related pages - [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/) — Features, supported languages, and installation - [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) - [Configure keelson.yaml](https://keelson.dev/docs/building-apps/keelson-yaml/) - [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) - [Framework notes](https://keelson.dev/docs/building-apps/frameworks/) --- # Configure keelson.yaml Understand the configuration your AI agent creates and the settings to review URL: https://keelson.dev/docs/building-apps/keelson-yaml/ Language: en --- `keelson.yaml` tells Keelson how to start your app, whether it needs a database, and which tasks to run on a schedule. Place it in the root folder of the app you deploy (the project root). **Your AI agent normally creates and updates this file when you ask it to deploy.** This page explains the settings worth reviewing. For all fields and their detailed requirements, see the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/). ## Example configuration This Python web app uses a Keelson database. ```yaml slug: my-app # App identifier runtime: python-slim # Python runtime command: "python app.py" # Command that starts the web server db: mode: libsql # Use Keelson Managed SQLite ``` | Field | What to check | |---|---| | `slug` | The name that identifies the app. It forms part of `https://--.keelson.run` | | `runtime` | The runtime for your app's language: Python, Node.js, or Go | | `command` | The command that starts the app's web server. Dependencies are installed during the build | | `db.mode` | Use `libsql` for a Keelson database, or `none` if you do not need one or use an external database | `db.mode: libsql` provisions a database. Your app also needs code to connect to it and read and write data. This setting alone does not switch an existing SQLite file to Managed SQLite. An app that only uses the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) can use `db.mode: none`. No configuration is needed to declare Files or Media usage. ## Settings for each app structure Every app needs `slug`, `runtime`, and `db.mode`. The other fields depend on its structure. | App structure | Main settings | |---|---| | Web app | Set `command` to the startup command | | Static site / SPA | Set `type: web` and `assets`; omit `command` | | Static frontend + API server (hybrid) | Set both `assets` and `command` | | Scheduled jobs only | Set `crons`; omit the web server `command` | You can add `crons` to a web app with server-side processing. In that case, do not set `type: web`: `type: web` and `crons` cannot be combined. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/). ### Static site / SPA This example serves prebuilt files. Set `assets.dir` to the build output folder. ```yaml slug: my-site type: web runtime: node-slim db: mode: none assets: dir: dist fallback: index.html # Return index.html when an SPA route is opened directly ``` Build static sites locally before deploying. Keelson does not install dependencies or run a build for them. Omit `fallback` for a regular static site if missing pages should not fall back to `index.html`. ### Static frontend + API server This example sends requests under `/api` to the server and serves prebuilt frontend files for other paths. For this structure too, build the frontend locally and put its output in `assets.dir` before deploying. Keelson builds the server-side code, but the files served as `assets` are the ones you upload. ```yaml slug: my-app runtime: node-slim command: "node server.js" db: mode: libsql assets: dir: dist fallback: index.html api: /api ``` ## Optional settings | What you want to do | Setting | Learn more | |---|---|---| | Describe the app | `description` | [Reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#description) | | Pass configuration values to the app | `env` | [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) | | Use API keys or other secrets | Declare the required names in `secrets`. Do not write secret values here | [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) | | Change database tables or columns during deployment | `db.migrate` | [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) | | Run work at specific times or intervals | `crons` | [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) | | Set the page used to check startup | `health.path` | [Reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#health) | | Call the app's API from another system | Declare paths and methods in `auth.endpoints`, and issue an app token separately | [External integrations](https://keelson.dev/docs/building-apps/external-integrations/) | ## Before deploying - **Check the app name and storage choice.** Make sure `slug` identifies the intended app and `db.mode` matches the database you want to use. - **Check for API keys or passwords written directly in the file.** Declare only names and descriptions in `secrets`; set the values through the console or another supported method. - **Check recurring work.** Review the `crons` schedules instead of relying on timers inside the app. You can ask your agent to check the startup command and dependency files. This command checks the configuration and files to be uploaded without uploading them: ```bash keelson deploy --check --json ``` This check alone does not confirm that the app will start or fit within your plan's limits. After deployment, open the app URL and test it. ## After changing the configuration Changes to `keelson.yaml` take effect on the next deployment. Ask your agent to update the configuration and redeploy. Keelson sets environment variables such as `PORT`. When reusing an older configuration, remove or replace retired fields. See the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) for required changes and validation errors. ## Next steps - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) - [Deploy your app](https://keelson.dev/docs/deploy/deploy/) - [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) — Runtime versions and constraints --- # Keelson SDK An overview of Files, Media, Identity, Tasks, and Email, with installation instructions for Python, Node.js, and Go URL: https://keelson.dev/docs/building-apps/sdk/ Language: en --- The Keelson SDK provides libraries for using file storage, user information, background tasks, email sending, and other Keelson features from your app's code. **It is available for Python, Node.js (JavaScript / TypeScript), and Go. Use only the features you need.** You do not need the SDK just to deploy an app. Add it when you want to keep uploaded images, for example, or tailor the app to the signed-in user. When asking an AI agent to implement a feature, tell it your app's language and what you want to achieve, and share this page or the relevant guide. ## Available features | Feature | What it does | Guide | |---|---|---| | **Files SDK** | Save, read, update, delete, and list private files used inside the app, such as configuration JSON or records of processed data | [Files and media: Files](https://keelson.dev/docs/building-apps/files-and-media/#files-sdk) | | **Media SDK** | Store images, PDFs, and attachments, and serve them by URL to members who can view the app. Use it for product photos or application forms | [Files and media: Media](https://keelson.dev/docs/building-apps/files-and-media/#media-sdk) | | **Identity SDK** | Read the signed-in user's information, group memberships, and workspace member information. Use it to record authors or vary behavior by group | [Use signed-in user information](https://keelson.dev/docs/building-apps/using-identity/) | | **Tasks SDK** | Enqueue a command declared under `tasks` in `keelson.yaml` to run on a separate instance, and check its status. Use it for work that continues after a request, such as generating PDFs or heavy external API calls | [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) | | **Email SDK** | Send email to the app's users and receive delivery results (delivered, bounce, complaint). Use it for password resets or request confirmations | [External integrations: Send email](https://keelson.dev/docs/building-apps/external-integrations/#send-email) | Use [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) for customer records, projects, and other data that needs querying or concurrent updates. Connect with a libSQL client for your language, installed separately from the Keelson SDK. ## Installation Add dependencies for your app's language. The core features are shared across all three languages, but function names, arguments, and helper features vary. ### Python All features are bundled in `keelson-sdk`. ```bash pip install keelson-sdk ``` Import the feature you need, for example `from keelson import files`. Available modules are `files`, `media`, `identity`, `tasks`, and `email`. Record the dependency in `requirements.txt` or `pyproject.toml`, according to your dependency manager. ### Node.js (JavaScript / TypeScript) Each feature has its own package. Install only those you need. | Feature | Install | |---|---| | Files | `npm install @keelsonhq/files` | | Media | `npm install @keelsonhq/media` | | Identity | `npm install @keelsonhq/identity` | | Tasks | `npm install @keelsonhq/tasks` | | Email | `npm install @keelsonhq/email` | Import a package with, for example, `import * as files from "@keelsonhq/files"`. ### Go One module contains packages for each feature. ```bash go get github.com/keelsonhq/go-sdk ``` Import the package you need, such as `github.com/keelsonhq/go-sdk/files`. The module includes `files`, `media`, `identity`, `tasks`, and `email`; use `directory` to list members and groups. ## Use the SDK on Keelson Use the SDK in server-side code, not in JavaScript running in the browser. Static sites and SPAs have no server, so they cannot use the SDK. To use these features, add an API server that calls the SDK and deploy as a hybrid app. Keelson provides connection details and credentials through environment variables for each feature. The SDK normally reads them automatically, so you do not need to put tokens or storage locations in your code. - **Files / Media:** No additional `keelson.yaml` configuration is needed. - **Identity:** No setup is needed to read basic user information from headers. To retrieve groups or member information, [enable the Directory API and redeploy](https://keelson.dev/docs/building-apps/using-identity/#setup). - **Email:** No additional `keelson.yaml` configuration is needed. The sender is `@mail.keelson.run` by default; on the Team plan, apps can also [send from your own domain](https://keelson.dev/docs/building-apps/external-integrations/#send-from-your-own-domain). - **Tasks:** Declare the tasks you enqueue under `tasks` in `keelson.yaml` and deploy. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/#declare-a-task). ## Local development Without Keelson connection settings, Files and Media save to local folders. With `keelson dev serve`, Identity receives the same user information headers as in production, and you can switch users (without the CLI, `KEELSON_LOCAL_MODE=1` makes it return test user information). Tasks runs the command synchronously on your machine through the CLI's `keelson dev task run` (see [Run locally](https://keelson.dev/docs/building-apps/background-tasks/#run-locally)). Email cannot send from your machine; without Keelson connection settings, sending raises an error. Local files and test user information are not automatically carried over to your deployed app. Files has language- and OS-related considerations. See [Files and media: local development](https://keelson.dev/docs/building-apps/files-and-media/#local-development) for storage locations and constraints, and [Run your app locally](https://keelson.dev/docs/building-apps/local-development/) for testing user information. The [environment variables reference](https://keelson.dev/docs/reference/environment-variables/) lists settings used by the SDK. --- # Database (Managed SQLite) Store business data and learn how to connect to, back up, and restore your database URL: https://keelson.dev/docs/building-apps/persistent-storage/ Language: en --- Use **Managed SQLite** to store customer records, projects, requests, and other data your app creates, searches, and updates. Keelson provisions a dedicated database for each app, and saved data survives restarts and redeployments. You can also connect to an external database you already use. ## What to store where Images and configuration files also have storage options that survive restarts. Choose based on what you are storing and how you use it. | Data and use case | Storage | |---|---| | Create, search, and update customer records, projects, or requests | **Managed SQLite** | | Read and write configuration files or records of processed data inside the app | **[Files SDK](https://keelson.dev/docs/building-apps/sdk/)** | | Display images or let users open PDFs and attachments | **[Media SDK](https://keelson.dev/docs/building-apps/sdk/)** | For example, a project tracking app can keep project names and assignees in the database and attached PDFs in Media. Store each attachment's ID in the database to link the file to its project. See [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) for file storage instructions. ## Local files are temporary Here, “local files” means **files written inside the server running your app on Keelson**. They are lost when the app restarts, is redeployed, or stops after a period without traffic. This is called “ephemeral” storage. Changing the folder to `./data.db` or `/tmp/data.db` does not preserve the data across restarts (`/data` does not exist: Keelson does not create it and the app cannot create it). Scheduled jobs run in a separate environment from the web app and cannot share these files either. You can use `/tmp` for files needed only during processing, such as incoming uploads or intermediate conversions. Save anything you need later in one of the persistent stores above. ### Apps using SQLite files Managed SQLite is SQLite-compatible, so the SQL and table definitions of an app built on SQLite usually stay as they are. What changes is the connection: replace the places where `sqlite3` or `better-sqlite3` write directly to a file inside the server with a libSQL client connection. The client APIs differ: Node.js moves from the synchronous `better-sqlite3` to the asynchronous `@libsql/client`, so add `await` in each handler. Python's `libsql` differs from `sqlite3` in `row_factory`, type adapters, and exception classes. In Python / Node.js, pass the same client a file such as `file:local.db` and it works as file-based SQLite in local development. If your app uses `sqlite3` or `better-sqlite3` to save data that must persist, you can ask your AI agent: > Make this app's data survive restarts and redeployments on Keelson. Find any file-based SQLite usage, and explain the changes needed for Managed SQLite and how to move existing data. Deployment checks reject detected SQLite file usage unless it is declared. For regenerable caches only, you can declare temporary SQLite use with `db.local_sqlite`. This does not make the files survive restarts. See the [reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#db). ## Use Managed SQLite Add this setting to `keelson.yaml` to provision a dedicated database for the app. Your AI agent normally writes both the configuration and connection code. ```yaml db: mode: libsql ``` - **No database server setup is needed.** Keelson supplies the connection URL and authentication token through the app's environment variables. - **Each app's data is separate.** Its credentials can access only its own database. - **Committed data survives restarts.** Data is saved when the SQL commit completes. - **The database is hosted in the same region as the app.** Japan is currently the only available region ([`region`](https://keelson.dev/docs/reference/keelson-yaml-reference/#region)). US West is in preparation. - **You can restore from backups.** Daily backups, manual backups, and point-in-time recovery are supported. ### Connection code In addition to changing the configuration, your app must connect through a libSQL client. SQL works as in SQLite. On Keelson the connection uses the URL and authentication token supplied by Keelson instead of a local file path. In Python / Node.js, fall back to `file:local.db` when the environment variable is absent, and the same code runs as file-based SQLite in local development (the Go libSQL client cannot open `file:`, so this pattern does not apply to Go). Compatible clients are available for Python, Node.js, and Go. These examples create a table and save one record. Add `libsql` to your Python dependencies or `@libsql/client` to your Node.js dependencies. #### Python ```python import os import libsql conn = libsql.connect( database=os.environ.get("KEELSON_DB_URL", "file:local.db"), # file: = local development auth_token=os.environ.get("KEELSON_DB_AUTH_TOKEN", ""), ) conn.execute(""" CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) conn.execute("INSERT INTO items (name) VALUES (?)", ("Sample",)) conn.commit() ``` #### Node.js ```javascript import { createClient } from "@libsql/client"; const db = createClient({ url: process.env.KEELSON_DB_URL ?? "file:local.db", // file: = local development authToken: process.env.KEELSON_DB_AUTH_TOKEN, }); await db.execute(` CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `); await db.execute({ sql: "INSERT INTO items (name) VALUES (?)", args: ["Sample"] }); ``` ### Add or change tables and columns A database's “schema” defines its structure: table and column names, data types, and constraints. If a table does not exist yet, create it with `CREATE TABLE IF NOT EXISTS`, as in the examples above. To change tables or columns during deployment, set a command in `db.migrate`. This process is called a “migration.” ```yaml db: mode: libsql migrate: "python migrate.py" ``` `db.migrate` runs before switching to the new code. If it fails, the app continues serving with the previous code. Any database changes already made are not automatically undone. Make the command safe to repeat without duplicating tables or data (idempotent). The updated database must remain readable and writable by the previous code. Static sites without `command` do not run `db.migrate`. See [Update and restore](https://keelson.dev/docs/deploy/update-and-restore/). ### Concurrent writes SQLite processes writes one at a time. Simultaneous writes from a web app and scheduled jobs, for example, may have to wait. If concentrated writes affect response times, review the workload or consider an external database such as PostgreSQL. ## Use an external database To connect to existing PostgreSQL, MySQL, or external libSQL, set `db.mode: none`. Provide the connection details yourself and set them as secrets. This setting does not create an external database or move existing data. ```yaml db: mode: none secrets: items: - name: DATABASE_URL description: "PostgreSQL connection string" required: - all_of: [DATABASE_URL] ``` Django deploys only through this external-database route (Managed SQLite has no backend for the Django ORM). See [Framework notes](https://keelson.dev/docs/building-apps/frameworks/#django). Common reasons to use an external database include: | Situation | Option | |---|---| | Data volume or performance needs exceed what Managed SQLite can handle | External PostgreSQL | | Very high concurrent write volume | PostgreSQL or MySQL | | Direct access from BI tools or core business systems | Shared PostgreSQL or a data warehouse | | Backup retention longer than your Keelson plan provides | An external managed database | ## Backups and recovery All plans include backup and recovery to help recover from accidental deletion or updates. | Method | Use | |---|---| | Daily backups | Restore from an automatically saved daily backup | | Manual snapshots | Create a backup when needed, such as before a major change (up to 5 per app per day, using UTC dates) | | Point-in-time recovery (PITR) | Restore data to a selected time within the recovery window | | Download | Save a backup or data from a selected time locally | ### Backup retention and recovery windows | | Starter | Plus | Team | |---|---|---|---| | Daily backups retained | 1 | 3 | 7 | | Point-in-time recovery window | Last 24 hours | Last 7 days | Last 14 days | Open **Restore points** on the app's console page and select a backup or time on the timeline to restore or download it. Recovery applies to one app and does not affect others. The state before recovery is kept for 72 hours, during which you can undo the recovery. Performing a recovery requires approval in the browser. ### What recovery includes These backups restore **only Managed SQLite data**. They do not include [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) files, external databases, app code, or configuration. For example, restoring an attachment ID in the database does not bring back a file already deleted from Media. See [Update and restore](https://keelson.dev/docs/deploy/update-and-restore/) for reverting changes that include code and configuration. ## Capacity - Managed SQLite has no fixed per-plan capacity limit, but underlying technical limits still apply. App execution may be restricted or suspended if usage significantly exceeds normal business workloads or affects platform stability, other customers, or reasonable operating costs. In emergencies, restrictions may be applied without prior notice. - Workspace storage has a limit for each plan. It includes Files / Media files, snapshots, deployment files, and similar data. Database capacity is separate. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for plan-specific limits. ## Related pages - [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) - [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) — Share state between the web app and jobs - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#db) --- # Files and media Choose Files for internal app data or Media to display images and attachments through URLs URL: https://keelson.dev/docs/building-apps/files-and-media/ Language: en --- Use the Files SDK or Media SDK to save files that need to survive app restarts and redeployments. For an overview, see [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/). **Use Files for files your app reads and writes internally, and Media for files displayed in the app or opened by users.** Media provides URLs that browsers can open directly, making it suitable for images and attachments. ## Choose by use case | | Files SDK | Media SDK | |---|---|---| | Main use | Save and update files used in internal app processing | Display images and let users open PDFs or attachments | | Examples | Configuration JSON, lists of processed URLs, intermediate summary files | Product photos, application PDFs, user-facing reports | | Reading | Server-side code reads through the SDK. No serving URL is provided | Server-side code can read files, and browsers can open them by URL | | Updates | Overwrite the same name (key). Aim for roughly one update per key per second | Each newly saved file gets an ID; existing files cannot be overwritten | | Maximum file size | 10 MiB | 50 MiB | Choose based on how a file is used, not just its format. A CSV used internally for calculations fits Files; a CSV report delivered to users fits Media. Media URLs are not public links that anyone can open. **Only members who can view the app can open them after signing in to Keelson.** Neither Files nor Media requires additional `keelson.yaml` configuration. Media access is controlled at the app level. It does not automatically enforce per-file permissions, such as allowing users to see only their own attachments. For those cases, consider storing files in Files and having the app check permissions before returning them. ## Keep files after a restart Data written to local files while the app runs does not survive restarts or redeployments. Writing to `/tmp` or the app directory alone does not preserve files for later use. Once a Files / Media SDK save completes, the file survives app restarts. No later synchronization step is needed. Use [Managed SQLite](https://keelson.dev/docs/building-apps/persistent-storage/) for customer records, projects, and other data you search and update by field. ## Installation Follow [Keelson SDK installation](https://keelson.dev/docs/building-apps/sdk/#installation) to add Files or Media to your app's dependencies. The examples below run on the server. You can usually ask your AI agent to implement storage by explaining what files you want to save and how they will be used. ## Files SDK Files works well for files that your app saves under a name and updates under that same name. Saved files can be shared between the web app and its scheduled jobs. The storage name is called a “key,” such as `settings.json` or `reports/2026-08.csv`. The main Node.js / Python operations are `write` to save, `read` to read, `delete` to remove, and `list` to list keys. If a file does not exist, `read` returns `null` in Node.js or `None` in Python. This example saves and reads settings, lists reports, and deletes an old report. ```javascript import * as files from "@keelsonhq/files"; await files.write("settings.json", JSON.stringify({ theme: "dark" })); const raw = await files.read("settings.json"); // Uint8Array | null const settings = raw ? JSON.parse(new TextDecoder().decode(raw)) : {}; const keys = await files.list("reports/"); // ["reports/2026-08.csv", ...] await files.delete("reports/2026-07.csv"); ``` ```python import json from keelson import files files.write("settings.json", json.dumps({"theme": "dark"})) settings = json.loads(files.read("settings.json") or "{}") # read() -> bytes | None keys = files.list("reports/") files.delete("reports/2026-07.csv") ``` When moving from local files, change both reads and writes to use the SDK. Files does not provide serving URLs. To deliver a saved file to users, implement a download handler in your app or use Media. If multiple processes read and update the same key, one update may overwrite another. Use database transactions for processing status, counters, or other data that needs concurrent updates. ## Media SDK Media is suitable for images displayed in the app and PDFs or attachments users open. Keelson serves saved files through URLs, so your app does not need its own file-serving handler. ### Save a file and display it 1. Call `put` in server-side code to save the file. 2. Store the returned file ID in the database alongside the relevant project, product, or other record. 3. Get the URL with `url(id)` and use it in an image or link. When the browser opens the URL, Keelson checks authentication and returns the file. The following server-side function saves an image. Pass the uploaded image bytes as its argument. ```javascript import * as media from "@keelsonhq/media"; async function savePhoto(imageBytes) { const id = await media.put(imageBytes, { contentType: "image/png", filename: "photo.png", }); // Store id in the database and return url to the frontend. return { id, url: media.url(id) }; } ``` ```python from keelson import media def save_photo(image_bytes): file_id = media.put( image_bytes, content_type="image/png", filename="photo.png", ) # Store file_id in the database and return url to the frontend. return {"id": file_id, "url": media.url(file_id)} ``` On Keelson, URLs have the form `/__keelson/media/`. For example, set an image's URL as an `` element's `src`, or a PDF's URL as a link's `href`. ```html Product photo Open application form ``` Keelson handles the file read. Knowing the URL is not enough to open it; the user needs permission to view the app. ### Updates, deletion, and other operations Saved files cannot be overwritten. To replace a file, save a new one and update the ID in the database. Delete the old file separately when it is no longer needed. - Use `get(id)` to read contents on the server, and `stat(id)` to check the Content-Type and size. - Use `exists(id)` to check existence. Delete with `del(id)` in Node.js or `delete(id)` in Python. - If you omit Content-Type, it is inferred from the filename extension. If it cannot be inferred, it defaults to `application/octet-stream`. ## Local development Without Keelson connection settings, both SDKs save files in local folders. These local files are not automatically transferred to Keelson Files / Media when you deploy. - Files writes actual files under `./.keelson/files/`. Add `.keelson/` to `.gitignore`. - Media writes under `./media/` (configurable with `MEDIA_DIR`). On Keelson, the platform sets the required environment variables. In Media local mode, the default URL is `/media/`. To preview files in a browser, add a handler to your development server that serves them. `media.url()` returns a URL; it does not start a local HTTP server. The Files and Media SDKs support local mode on Linux, macOS, and Windows for Node.js, Python, and Go. The Files SDK maps keys directly to local paths. On Windows and macOS filesystems configured as case-insensitive, keys that differ only by case, such as `Report` and `report`, cannot coexist. Path-based backends also have a race if another process replaces a checked component with a symlink or junction before the operation completes. Use them for trusted, single-user local development. The production GCS path does not have this limitation. ## File management and capacity - The console and CLI do not provide file listing, downloading, or replacement for app files. Build a file list in your app for users. For Media, create links from saved IDs; for Files, implement a download handler when needed. - Store files that exceed the size limits directly in external object storage, such as S3-compatible storage. Outbound connections are unrestricted, except for the SMTP ports. Files / Media files count toward workspace storage capacity ([Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/)). Managed SQLite backups do not include Files / Media file contents. Restoring a database does not restore deleted files. ## Related pages - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) — Use the Files SDK from scheduled jobs --- # Scheduled jobs Run tasks at set times or intervals and check their results URL: https://keelson.dev/docs/building-apps/scheduled-jobs/ Language: en --- Run tasks at set times or intervals, such as a report every morning or data synchronization every hour. These are called “scheduled jobs.” They run even when nobody is using the web app. Tell your AI agent what should run and when, and it can configure `crons` in `keelson.yaml`. > Add a scheduled job that summarizes the previous day's sales at 9 a.m. every morning in the workspace's timezone. Make the results available in the logs. ## Basics This example adds a job to a web app that runs `report.py` every day at 9 a.m. ```yaml slug: my-app runtime: python-slim command: "python app.py" db: mode: libsql crons: - name: daily-report description: "Email yesterday's sales report" schedule: "0 9 * * *" # Every day at 09:00 command: "python report.py" timeout: 120 # Limit each run to 120 seconds ``` | Field | Meaning | |---|---| | `name` | Job name, using lowercase letters, numbers, and hyphens | | `schedule` | Cron expression specifying the time or interval | | `command` | Command that performs one run of the task and exits | | `timeout` | Maximum duration per run in seconds. The job stops when this limit is reached | | `enabled` | Set to `false` to stop scheduled runs. Defaults to `true` | | `description` | Optional. A one-line explanation of what the job does, shown in the Description column of the console's Scheduled jobs tab. Leading and trailing whitespace is trimmed, and an empty value counts as unset. Up to 100 characters. It is display only and does not change what runs | `name`, `schedule`, and `command` are required. Set `timeout` between 1 and 600 seconds, within your plan's limit. If omitted, it defaults to 300 seconds or your plan's limit, whichever is shorter. Explicitly setting a value above the limit causes deployment to be rejected. You can also create an app containing only scheduled jobs. Omit the top-level `command` in that case. Apps with only scheduled jobs do not count toward the concurrent app limit. ## Write a schedule `schedule` uses a five-field cron expression to specify times or intervals. | Schedule | Cron expression | |---|---| | Every day at 09:00 | `0 9 * * *` | | Weekdays at 09:00 | `0 9 * * 1-5` | | Every hour, on the hour | `0 * * * *` | | Every 15 minutes (Plus / Team) | `*/15 * * * *` | | Every 5 minutes (Team) | `*/5 * * * *` | | Midnight on the first day of each month | `0 0 1 * *` | Schedules are evaluated in **the workspace's timezone**. It is set from the browser when the workspace is created and cannot be changed later. A run may start later than its scheduled time. The minimum interval is 60 minutes on Starter, 15 minutes on Plus, and 5 minutes on Team. Shorter schedules are rejected during deployment. Running every minute is not supported.
Cron expression fields ```text ┌───────────── Minute (0–59) │ ┌─────────── Hour (0–23) │ │ ┌───────── Day of month (1–31) │ │ │ ┌─────── Month (1–12) │ │ │ │ ┌───── Day of week (0–6, 0 = Sunday) * * * * * ``` `*` means every value. For example, `0 9 * * *` means 09:00 on every day of every month, regardless of the day of the week.
## Results and logs On the app's console page, you can see each job's run history, success or failure, and logs. Use **Run now** to test a job without waiting for the next scheduled time. If a run fails, check its logs, required secrets and configuration, and whether it can finish within `timeout`. You can share the logs with your AI agent and ask it to investigate and fix the problem. Standard output and standard error are each recorded up to 256 KiB per run. Output beyond that is truncated. ### Monthly run counts and total runtime Only runs that actually start are counted. Successful runs, failed runs, and **Run now** executions count; skipped runs do not. Total runtime is elapsed time, including time spent waiting for external APIs. If a run cannot be measured because of a platform failure, it consumes one run but does not count toward total runtime. Once either monthly limit is reached, remaining runs that month are skipped. A run that has already started is allowed to finish, and the allowances reset the following month. The maximum number of jobs at the minimum interval does not fit within the monthly limits. The job count, minimum interval, per-run duration, monthly run count, and total runtime are independent limits. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for the limits for each plan. ## Execution model A scheduled job performs one unit of work and exits. It is not a way to keep an app running continuously. | Situation | Behavior | |---|---| | Relationship to the web app | Runs in a separate environment; memory and local files are not shared | | Saving data | Files inside the execution environment do not carry over to the next run. Save persistent data in Managed SQLite or with the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) | | Previous run is still running | The next run of the same job is skipped. It is not queued for later | | Run fails | No immediate automatic retry. The next scheduled run proceeds as usual | | Time limit is reached | The job stops, even if it is partway through its work | Timers and schedulers inside an app do not run while the app is stopped. Use `crons` for work that must run on a schedule. Background work after returning a response is also not guaranteed to finish. Complete it during the request, or enqueue work that continues after the response as a [background task](https://keelson.dev/docs/building-apps/background-tasks/). See [How apps run](https://keelson.dev/docs/building-apps/how-apps-run/). ## Design rules ### Make repeated runs safe Prevent duplicate records or deliveries when an interrupted task is retried or someone uses **Run now** to repeat it. This property is called “idempotency.” For example, if `reports.date` has a primary key or UNIQUE constraint, you can update an existing record for the same date. ```python conn.execute(""" INSERT INTO reports (date, total) VALUES (?, ?) ON CONFLICT(date) DO UPDATE SET total = excluded.total """, (today, total)) conn.commit() ``` ### Process accepted work later without background tasks To run accepted work right after the request, use [background tasks](https://keelson.dev/docs/building-apps/background-tasks/) first. If you build it without background tasks, or a wait until the next run is acceptable, use the method below. To collect work in the web app and process it later, save pending work in Managed SQLite. A scheduled job reads the pending records, processes them, and records completion. ```yaml # Run every 5 minutes on the Team plan crons: - name: process-pending schedule: "*/5 * * * *" command: "python process_pending.py" timeout: 120 ``` - **Allow for the delay before work starts.** Work waits until the next run, so this is unsuitable for tasks that must start immediately. Tell users about the wait. - **Plan for repeated processing.** An action such as sending email may succeed, but the job may stop before recording completion in the database. - **Record failure counts and reasons.** Set a retry limit and decide what to do with repeatedly failing records. Keelson does not automatically retry or isolate individual pending records. Complete short tasks within the request before returning the response. ### Limit work per run When processing many records, cap the number handled in each run and leave the rest for the next one. Split work that exceeds the execution time limit across multiple runs. When calling external APIs, also respect that service's rate limits. Increase the interval or reduce the number of calls per run. ## Implementation examples ### Morning report (Python) This example assumes `sales` and `reports` tables already exist and `reports.date` has a primary key or UNIQUE constraint. Add `libsql` to your dependencies. The example summarizes the previous day in Japan time. It assumes `sales.date` stores dates in `YYYY-MM-DD` format using the same timezone. Change `Asia/Tokyo` to the timezone used for your reports. The workspace schedule timezone does not automatically change the timezone your code uses to calculate dates. ```python # report.py import os from datetime import datetime, timedelta from zoneinfo import ZoneInfo import libsql conn = libsql.connect( database=os.environ["KEELSON_DB_URL"], auth_token=os.environ["KEELSON_DB_AUTH_TOKEN"], ) report_date = (datetime.now(ZoneInfo("Asia/Tokyo")).date() - timedelta(days=1)).isoformat() row = conn.execute( "SELECT COUNT(*), COALESCE(SUM(amount), 0) FROM sales WHERE date = ?", (report_date,) ).fetchone() conn.execute( "INSERT INTO reports (date, count, total) VALUES (?, ?, ?) " "ON CONFLICT(date) DO UPDATE SET count = excluded.count, total = excluded.total", (report_date, row[0], row[1]), ) conn.commit() print(f"Report complete: {report_date}, {row[0]} records / {row[1]}") ``` ### Sync with an external API (Node.js) Add `@libsql/client` to your dependencies and set the `SYNC_API_URL` environment variable. This example assumes a `synced_items` table exists with a primary key or UNIQUE constraint on `id`, and that the API returns an array of objects with an `id`. ```javascript // sync.js import { createClient } from "@libsql/client"; const db = createClient({ url: process.env.KEELSON_DB_URL, authToken: process.env.KEELSON_DB_AUTH_TOKEN, }); const res = await fetch(process.env.SYNC_API_URL); if (!res.ok) throw new Error(`Sync API error: ${res.status}`); const items = await res.json(); for (const item of items) { await db.execute({ sql: `INSERT INTO synced_items (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, args: [item.id, JSON.stringify(item)], }); } console.log(`Sync complete: ${items.length} records`); ``` ### Slack notifications This example assumes the notification logic is implemented in `notify.py`. Pass the webhook URL through a secret. ```yaml slug: daily-notifier runtime: python-slim db: mode: none secrets: items: - name: SLACK_WEBHOOK_URL description: "Slack Incoming Webhook for notifications" required: - all_of: [SLACK_WEBHOOK_URL] crons: - name: daily-notify schedule: "0 9 * * 1-5" command: "python notify.py" timeout: 30 ``` ## If deployment is rejected | Situation | What to do | |---|---| | `timeout` exceeds 600 seconds or your plan's limit | Reduce it to the limit or below, and split the task if necessary | | Schedule is shorter than your plan's minimum interval | Increase the interval | | Number of jobs exceeds your plan's limit | Combine tasks or remove unused jobs | | `description` is longer than 100 characters (`cron_description_too_long`) | Shorten it to one line of 100 characters or fewer. It is not truncated automatically | | `type: web` is combined with `crons` | Remove `type` for a jobs-only app. If adding work to a static site, also review whether it needs a server | | Retired `workers:` configuration remains | Remove it and move the work to `crons` or request handling as appropriate | ## When scheduled jobs are not suitable - Work that must start immediately after an event; a scheduled job waits until its next run. Use [background tasks](https://keelson.dev/docs/building-apps/background-tasks/) instead. - Work that cannot be split and exceeds the per-run time limit. - Workflow orchestration, such as starting one job only after another finishes. ## Related pages - [How apps run](https://keelson.dev/docs/building-apps/how-apps-run/) - [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) - [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#crons) --- # Background tasks Run heavy work after a request on a separate instance URL: https://keelson.dev/docs/building-apps/background-tasks/ Language: en --- Run slow work that follows a request, such as generating a PDF or making a heavy external API call, on an instance separate from the web app. This is called a "background task" (or "task" on this page). Declare a command under `tasks` in `keelson.yaml`, then enqueue it from your app's code with the [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/) `enqueue` function. Keelson starts the command once and retries it automatically if it fails. Tell your AI agent what should continue after the request, and it can write the `tasks` declaration and the code that enqueues it. > When an order is placed, generate a receipt PDF and email it as a background task. Make sure running the same task twice does not create duplicate PDFs or emails. ## When to use | Work | What to use | |---|---| | Short work that finishes within the request, such as saving a form | Complete it while handling the request | | Work that runs at set times, such as a morning report | [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) (`crons`) | | Work that a request starts and that runs afterward | Background tasks (`tasks`) | Tasks suit work such as: - Work that does not fit within the HTTP request limit (120 seconds) - Heavy external API calls (AI generation, sending large amounts of data, and so on) - Generating PDFs or images, or sending bulk email Work that continues inside the app after the response is sent (such as FastAPI's `BackgroundTasks`) is not guaranteed to finish. Move that work to a task. ## Declare a task Write the task name and the command to run under `tasks` in `keelson.yaml`. ```yaml slug: my-app runtime: python-slim command: "python app.py" db: mode: libsql tasks: - name: generate-pdf description: "Generate the invoice PDF for an order" command: "python generate_pdf.py" timeout: 180 # limit each attempt to 180 seconds (valid on every plan) max_attempts: 3 # try up to 3 times, including the first attempt ``` | Field | Meaning | |---|---| | `name` | Task name, using lowercase letters, digits, and hyphens. You pass this name when enqueueing | | `command` | Command that performs one run of the work and exits | | `timeout` | Time limit for one attempt, in seconds. Defaults to 300 seconds (or your plan's limit, if lower). A value above your plan's limit is rejected at deploy | | `max_attempts` | Maximum number of attempts, including the first. Defaults to 3 | | `description` | Optional. A one-line explanation of what the task does, shown in the console's Background tasks tab. Leading and trailing whitespace is trimmed, and an empty value counts as unset. A value longer than 100 characters is rejected at deploy (`task_description_too_long`; it is not truncated). It is display only and does not change what runs | `name` and `command` are required. See the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) for allowed ranges and validation rules. ## Enqueue and check status From your app's code, enqueue a task by its declared name with input data. The call returns right away with the task ID (`task_id`). See [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/#installation) for installation. **Python** ```python from keelson import tasks task_id = tasks.enqueue("generate-pdf", {"order_id": 1}, idempotency_key="order-1-pdf") status = tasks.get(task_id) print(status.status, status.claimed_attempts, status.last_failure_code) try: tasks.enqueue("generate-pdf", {"order_id": 2}) except tasks.TasksError as e: if e.code == "TASK_MONTHLY_QUOTA_EXCEEDED": ... # this month's limit is used up ``` **Node.js** ```ts import { enqueue, get, TasksError } from "@keelsonhq/tasks"; const taskId = await enqueue("generate-pdf", { order_id: 1 }, { idempotencyKey: "order-1-pdf", }); const status = await get(taskId); console.log(status.status, status.claimed_attempts, status.last_failure_code); try { await enqueue("generate-pdf", { order_id: 2 }); } catch (e) { if (e instanceof TasksError && e.code === "TASK_MONTHLY_QUOTA_EXCEEDED") { // this month's limit is used up } } ``` **Go** ```go import "github.com/keelsonhq/go-sdk/tasks" client, err := tasks.New() if err != nil { return err } taskID, err := client.Enqueue(ctx, "generate-pdf", map[string]int{"order_id": 1}, tasks.WithIdempotencyKey("order-1-pdf")) if err != nil { var te *tasks.Error if errors.As(err, &te) && te.Code == "TASK_MONTHLY_QUOTA_EXCEEDED" { // this month's limit is used up } return err } status, err := client.Get(ctx, taskID) ``` When you pass an `idempotency_key`, enqueueing again with the same task name and key does not create a new task; it returns the first task's ID. A key is 1–128 printable ASCII characters. `get` returns one of five statuses (`status`): | Status | Meaning | |---|---| | `queued` | Waiting. Includes waiting to start and waiting to retry | | `running` | Running | | `succeeded` | Succeeded (exit code 0) | | `failed` | Failed. `last_failure_code` gives the reason ([When a task fails](#when-a-task-fails)) | | `cancelled` | Cancelled before it started | `claimed_attempts` is the number of attempts started, and `last_failure_code` is the reason the last failed attempt failed. To show completion in your UI, have the task command write its result to the database and have the UI read a status column there. Use `get` as a supplementary check. ## Write the command A task command is an ordinary program. When Keelson starts the command, it writes one line of JSON to **standard input** and then closes it: ```json {"attempt_no":1,"payload":{"order_id":1},"schema_version":1,"task_id":"…","task_name":"generate-pdf"} ``` | Key | Contents | |---|---| | `attempt_no` | Which attempt this is (starting at 1) | | `payload` | Input data passed at enqueue time (`null` if omitted) | | `schema_version` | Version of this input format. Currently `1` | | `task_id` | Task ID. Stays the same across retries | | `task_name` | Task name | The attempt succeeds if the command exits with **exit code 0**; any other result is a failure. **Python** ```python import json import sys doc = json.loads(sys.stdin.readline()) order_id = doc["payload"]["order_id"] # ... generate and save the PDF ... # An uncaught exception exits non-zero and counts as a failure ``` **Node.js** ```ts import { readFileSync } from "node:fs"; const doc = JSON.parse(readFileSync(0, "utf8")); const orderId = doc.payload.order_id; // ... generate and save the PDF ... // An uncaught exception exits non-zero and counts as a failure ``` **Go** ```go var doc struct { AttemptNo int `json:"attempt_no"` Payload json.RawMessage `json:"payload"` SchemaVersion int `json:"schema_version"` TaskID string `json:"task_id"` TaskName string `json:"task_name"` } if err := json.NewDecoder(os.Stdin).Decode(&doc); err != nil { log.Fatal(err) // exits with code 1 and counts as a failure } // ... generate and save the PDF ... ``` Tasks do not share a local disk with the web app. Save results to the [database](https://keelson.dev/docs/building-apps/persistent-storage/) or with the [Files / Media SDK](https://keelson.dev/docs/building-apps/files-and-media/). The command can read the same environment variables as a scheduled job. ## Make it safe to run twice Tasks run **at least once**. Failed attempts are retried automatically, and even after a successful attempt, the command can run again if its completion does not reach Keelson. Writing the command so that running it more than once does not corrupt the result (making it idempotent) is your app's responsibility. Follow these three rules: 1. **Compare state before changing it:** do "mark as processing if still pending" in a single `UPDATE`. If no rows were updated, another attempt is already handling it, so exit. ```sql UPDATE orders SET pdf_status = 'processing' WHERE id = ? AND pdf_status = 'pending'; ``` 2. **Pass an idempotency key to external APIs:** for external APIs where receiving the same request twice is a problem, such as payments or email, pass an idempotency key. The `task_id` works, because it stays the same across retries. 3. **Use fixed output file names and row keys:** save under names derived from the input, such as `receipts/order-1.pdf`. Names that include timestamps or random values produce two outputs when the command runs twice. ## Always process a saved order If you want "every saved order also enqueues a receipt task," you cannot put saving the order and enqueueing the task in one transaction: the order is stored in your app's database, while the task is recorded on Keelson. If the app stops right after saving, the order remains but the task is never enqueued. To prevent this, write a row for "a task not yet enqueued" (an outbox row) to your app's database in the same transaction as the order, then enqueue after the commit. This is commonly called the outbox pattern. 1. In one transaction, write the order row and an outbox row (ID, task name, input, and an enqueued flag) 2. After the commit, call `enqueue` with the outbox row's ID as the `idempotency_key` 3. When the enqueue succeeds, mark the outbox row as enqueued 4. Rows whose enqueue failed, or whose app stopped before marking them, are sent again with the same key on the next request or by a scheduled job. Because the key is the same, the task is never enqueued twice ```text Web (request handling) App database Keelson (tasks) │ ① Write the order row + outbox row in one transaction │ ─────────────────────────────▶ │ │ ② enqueue(name, input, idempotency_key = outbox row ID) │ ────────────────────────────────────────────────────────────▶ │ │ ◀──────────────────────────────────────────────── task_id ──── │ │ ③ Mark the outbox row as enqueued │ ─────────────────────────────▶ │ ④ Rows that failed or stopped before ②③ are resent with the same key by the next request or a cron (no duplicates) ``` ```python import json import uuid from keelson import tasks def place_order(conn, order_id, items): outbox_id = str(uuid.uuid4()) conn.execute("INSERT INTO orders (id, items) VALUES (?, ?)", (order_id, items)) conn.execute( "INSERT INTO task_outbox (id, task_name, payload, enqueued) VALUES (?, ?, ?, 0)", (outbox_id, "send-receipt", json.dumps({"order_id": order_id})), ) conn.commit() flush_outbox(conn) def flush_outbox(conn): # Also call this from later requests or a scheduled job rows = conn.execute( "SELECT id, task_name, payload FROM task_outbox WHERE enqueued = 0" ).fetchall() for outbox_id, name, payload in rows: tasks.enqueue(name, json.loads(payload), idempotency_key=outbox_id) conn.execute("UPDATE task_outbox SET enqueued = 1 WHERE id = ?", (outbox_id,)) conn.commit() ``` ## When a task fails **Automatic retries:** when the command ran and failed (`exit_nonzero`, `timed_out`, or `no_completion`) and the attempt count has not reached `max_attempts`, Keelson retries after waiting 10, 20, 40, and then 80 seconds. You cannot change these intervals. When the task ends before the command runs (`delivery_failed`, `manifest_unresolvable`, or `quota_exhausted`), it is not retried and becomes `failed` immediately. | `last_failure_code` | Meaning | |---|---| | `exit_nonzero` | The command exited with a non-zero exit code | | `timed_out` | The command exceeded its time limit (`timeout`) and was stopped | | `no_completion` | The deadline passed without a completion report | | `delivery_failed` | The attempt could not start within one hour of becoming ready to run | | `manifest_unresolvable` | Two or more deploys happened while the attempt was waiting, so the declaration it needed could not be resolved | | `quota_exhausted` | The monthly limit could not be reserved for a retry, or for an attempt starting in a new month | **Investigate:** on the app's **Background Tasks** tab in the console, you can see each task's status and the standard error of its last attempt (up to 256 KiB, with secret values masked). `get` in the SDK does not return standard error. **Try again:** there is no manual re-run. To try again, enqueue the task again from your app. If you use an idempotency key, use a different key from the original task (the same key returns the original task). **When tasks are cancelled:** the following actions change the affected `queued` tasks to `cancelled`. An attempt that is already running finishes, but it is not retried if it fails. - Manually suspending the app - Quarantining the app for abuse - Deleting the app (task records are deleted too) - A deploy that removes the name from `tasks` (only tasks with the removed name) You can also cancel a specific `queued` task, one at a time, from the console's **Background Tasks** tab. An app sleeping because it has no traffic does not cancel tasks. ## Limits | Item | Limit | |---|---| | Definitions | 10 per app | | Unprocessed (`queued`) tasks | 1,000 per app. Beyond that, enqueueing is rejected with `TASK_BACKLOG_LIMIT_EXCEEDED` | | Run time of one attempt | 300 seconds by default, 600 seconds maximum. Plan limits: Starter 180 seconds / Plus 300 seconds / Team and above 600 seconds | | Attempts | 3 by default, 5 maximum | | Input size | 65,536 bytes for the whole enqueue request (`payload` and `idempotency_key` as JSON). Larger requests fail with `TASK_PAYLOAD_TOO_LARGE` | | Input retention | Deleted 7 days after the task finishes | | Monthly limit | The run count and total run time of "Background Jobs," shared with scheduled jobs | If the default `timeout` (300 seconds) exceeds your plan's limit, it is lowered to the limit. Explicitly setting a value above the limit causes the deploy to be rejected. For the monthly limit, each started attempt counts as one run, including retries. Once the limit is reached, new tasks are rejected with `TASK_MONTHLY_QUOTA_EXCEEDED`. Enqueueing again with the same idempotency key still returns the first task, even at the limit. Each plan limits how many tasks run at the same time; tasks beyond that wait their turn. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for the monthly limit values. ## Where you can enqueue from You can enqueue tasks not only from web request handling but also from scheduled jobs and from task commands. For example, a nightly scheduled job can find the records to process and enqueue one task per record. Keelson handles authentication automatically. You do not need to put tokens in your code. ## Run locally Outside Keelson (when `KEELSON_MODE=local` is set, or when no Keelson environment variables are present), the SDK's `enqueue` starts the CLI's `keelson dev task run` as a child process and returns after the command finishes (synchronous execution). Local runs differ from production as follows: - `enqueue` does not return until the work finishes - The command runs as a child process on the same development machine, not on a separate instance. It can see local files that it would not see in production - There are no retries. If the command fails, `enqueue` does not raise an error; `get` returns `failed` - There is no concurrency limit, unprocessed task limit, or monthly limit - Run time is not lowered to a plan limit; the declared `timeout` is used as is - An idempotency key prevents duplicates only for sequential enqueues within the same process. It does not prevent them for concurrent enqueues, across processes, or after a restart - `get` finds only tasks enqueued within the same process Passing locally does not guarantee the same result in production, where retries and plan limits apply and an attempt can run more than once. Running locally requires a CLI version that includes `keelson dev task run`. With an older CLI, `enqueue` fails with `TASKS_LOCAL_CLI_FAILED`; update with `keelson upgrade`. You can also test just the command without going through the app: ```bash keelson dev task run generate-pdf --payload '{"order_id": 1}' ``` ## Related pages - [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) - [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/) - [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) - [Error codes and resolutions](https://keelson.dev/docs/reference/error-codes/) --- # Use signed-in user information Identify users, restrict operator screens, and implement group-based behavior and business permissions URL: https://keelson.dev/docs/building-apps/using-identity/ Language: en --- Keelson handles login and checks who can open your app. Your app can use the resulting user information to record who created a record or show users only their own data. Implement business rules, such as who can approve a request, in your app. Choose among these three approaches: | What you want to do | What to use | Additional setup | |---|---|---| | Identify the person accessing the app, record authors, or show personal data | User information headers such as `X-Keelson-User-Id` | None | | Show a screen only to app operators who can deploy or change settings | `X-Keelson-User-App-Perms` header | None; assign permissions in the console | | Vary behavior by department or job function, or identify approvers | `attributes.groups` from the [Identity SDK](https://keelson.dev/docs/building-apps/sdk/) | `keelson apps directory enable` | Use group memberships or your app's own data, such as an assignee table, for business permissions like “only a manager can approve.” The `manage` permission is for deploying and configuring an app; its holders are not necessarily business approvers. ## 1. Identify the current user For requests from signed-in users, Keelson provides user information in headers (metadata attached to the request). Read these headers in your server-side code. | Header | Content | |---|---| | `X-Keelson-User-Id` | User ID: a stable identifier to store in your database | | `X-Keelson-User-Email` | Email address, or an empty string if Keelson has no value | | `X-Keelson-User-Name` | Display name, or an empty string if Keelson has no value | To record the author, save the user ID alongside the data. To show only the user's own data, include that ID in your database query. Reading the header alone does not filter data automatically. ```python # FastAPI / Flask user_id = request.headers.get("X-Keelson-User-Id") email = request.headers.get("X-Keelson-User-Email") ``` ```javascript // Express const userId = req.headers["x-keelson-user-id"]; const email = req.headers["x-keelson-user-email"]; ``` Keelson adds this information after verifying the user's login. If a caller sends headers with the same names, Keelson replaces them with the verified user's information. Your app does not need to manage the Keelson login session. ### Read user information with the SDK The Identity SDK returns user information as `id`, `email`, and `name`. See [Keelson SDK installation](https://keelson.dev/docs/building-apps/sdk/#installation). Run the following examples on the server. `getCurrentUser` only reads headers. It makes no external request and does not require the Directory API to be enabled. In JavaScript, call it with `await`. A missing or empty user ID header raises `IdentityError`. ```javascript import { getCurrentUser } from "@keelsonhq/identity"; app.get("/", async (req, res) => { const user = await getCurrentUser({ headers: req.headers }); // { id, email, name } res.type("text").send(`Hello, ${user.name ?? "Guest"}`); }); ``` To also get the user's permissions for this app (`X-Keelson-User-App-Perms`), use `getRequestUser` (`get_request_user` in Python, `GetRequestUser` in Go). It returns the same values as `getCurrentUser` plus `perms` ([usage](https://keelson.dev/docs/building-apps/local-development/#get-the-current-user-and-permissions-getrequestuser)). ## 2. Operator-only screens Apps have two permissions, `view` and `manage`, assigned to groups through the console or CLI ([Groups and app access](https://keelson.dev/docs/workspace/groups-and-app-access/)). `manage` allows operators to deploy, change configuration, and edit secrets. Owner, Admin, and Developer have it by default. Keelson passes the resulting permissions in a header. | `X-Keelson-User-App-Perms` | Meaning | |---|---| | `view` | Can view the app | | `view,manage` | Can view and manage the app | Your app only needs to check whether `manage` is present. Do not hard-code group names for this check. ```javascript // Express: read inside the handler because headers vary by request. function canManage(req) { const perms = (req.headers["x-keelson-user-app-perms"] ?? "").split(","); return perms.includes("manage"); } app.get("/admin", (req, res) => { if (!canManage(req)) return res.status(403).end(); res.send("App operator screen"); }); ``` ```python # FastAPI / Flask: also read per request. def can_manage(request) -> bool: perms = request.headers.get("X-Keelson-User-App-Perms", "").split(",") return "manage" in perms ``` To make an app available only to the accounting team, assign permissions; no app code is needed ([Share an app with your team](https://keelson.dev/docs/access-control/share-with-team/)). To let only operators open a settings screen, check `manage`. You can change who has `manage` in the console without changing code. ## 3. Group-based behavior Use the Identity SDK to get group memberships when you need department-specific behavior or business permissions such as allowing only approvers to approve. Hiding controls in the UI is not enough. When implementing “users can see only their own requests,” filter lists and also enforce on the server that **users cannot retrieve or update someone else's request by specifying its ID in the API**. Base decisions on Keelson-provided headers or SDK results, not values supplied by the browser. ### Setup To retrieve group or member information, enable read access to the Directory API, then redeploy. Enabling it alone does not make it available to the running app. ```bash keelson apps directory enable keelson deploy ``` After redeployment, the app receives `KEELSON_DIRECTORY_TOKEN`, which the SDK uses automatically. Keep this token on the server; do not send it to the browser. ### Usage ```javascript import { getCurrentIdentity } from "@keelsonhq/identity"; // Read inside a request handler. const me = await getCurrentIdentity({ headers: req.headers }); const groups = me.attributes?.groups ?? []; // Example: ["everyone", "developers", "accounting"] const isAccountingMember = groups.includes("accounting"); // Use this result to decide whether to allow accounting operations. ``` ```python from keelson import identity me = identity.get_current_identity(headers=request.headers) groups = (me.attributes.groups or []) if me.attributes else [] is_accounting_member = "accounting" in groups # Use this result to decide whether to allow accounting operations. ``` `attributes.groups` contains **group keys**: - Role-based system groups: Owner receives `owners`, `developers`, and `everyone`; Admin receives `admins`, `developers`, and `everyone`; Developer receives `developers` and `everyone`; App User receives `everyone`. - Custom groups the user belongs to that have permissions assigned for this app. Groups not assigned to the app are omitted. - Keys are stable and can be used in code. Japanese keys, such as `経理` (accounting), are also supported. `getCurrentIdentity` queries the Directory API. Its result also includes user information (`user.id`, `user.email`, `user.name`), the workspace role (`workspace.role`), and app permissions (`app.permissions`). User information is nested differently from the result of `getCurrentUser`. ### Use the member list to choose assignees When you need information about other members, such as options in an assignee picker, use `listMembers` / `list_members` (with search and role or group filters), `getUser`, or `listGroups`. Each member has `id`, `email`, `name`, `role`, and `image_url`. `image_url` is the profile image URL (served from `img.clerk.com`), or `null` if the member has no image. Append `width` / `height` query parameters to get a smaller image. On the server, return only what the picker needs: ```javascript import { listMembers } from "@keelsonhq/identity"; // Inside a request handler. Returns up to 100 members per call. const page = await listMembers({ group_key: "developers", limit: 100 }); const options = page.items.map((m) => ({ id: m.id, label: m.name || m.email, image_url: m.image_url, // string or null })); ``` ```python from keelson import identity page = identity.list_members(group_key="developers", limit=100) options = [ {"id": m.id, "label": m.name or m.email, "image_url": m.image_url} for m in page.items ] ``` In the browser, show initials when there is no image or the image fails to load: ```javascript function avatar(option) { const initials = document.createElement("span"); initials.className = "avatar-initials"; initials.textContent = option.label.slice(0, 1).toUpperCase(); if (!option.image_url) return initials; const img = document.createElement("img"); img.className = "avatar"; img.alt = ""; const src = new URL(option.image_url); src.searchParams.set("width", "64"); src.searchParams.set("height", "64"); img.src = src.toString(); img.addEventListener("error", () => img.replaceWith(initials), { once: true }); return img; } ``` **Store only the member `id` in your app's database**, for example in a task's assignee column. Do not store `image_url`, `name`, or `email`. Look up the latest values with `listMembers` or `getUser` each time you display them; a member's image can change at any time, and a stored copy becomes stale. ## Local development When you run your app on your machine, there is no Keelson login, so requests carry no user information headers. Start it with `keelson dev serve` to receive the same headers as in production and switch between a view-only user and a user with manage permission. When you start it with `KEELSON_LOCAL_MODE=1` and no CLI, the Identity SDK returns one fixed user. See [Run your app locally](https://keelson.dev/docs/building-apps/local-development/). Custom groups cannot be reproduced locally. Also test group-based behavior in a test app with real permissions configured. ## Requests without a signed-in user Requests to `/api/external/` or `/api/webhooks/` authenticated with an app token do not carry a signed-in user's ID. Handle them separately from code that calls `getCurrentUser` or `getCurrentIdentity`. See [External integrations](https://keelson.dev/docs/building-apps/external-integrations/) for authentication details. ## Related pages - [Groups and app access](https://keelson.dev/docs/workspace/groups-and-app-access/) — Who receives view and manage permissions - [Authentication and login](https://keelson.dev/docs/access-control/auth-and-login/) --- # Run your app locally Run your app on your machine with keelson dev serve and switch between users to test it URL: https://keelson.dev/docs/building-apps/local-development/ Language: en --- When you run your app on your machine, there is no Keelson login, so requests carry no user information headers such as `X-Keelson-User-Id`. With `keelson dev serve`, your app receives the same headers as on Keelson, and you can switch between users to check its behavior. ## Two ways to run locally | | Through `keelson dev serve` | `KEELSON_LOCAL_MODE=1` (no CLI) | |---|---|---| | User | Switch to anyone in the user list | One fixed user | | How user information arrives | The same headers as in production | The SDK returns test data (no headers) | | Member list and groups (`listMembers` and so on) | Returned from the user list (no need to enable the Directory API) | Returned from the user list | | Requires | The latest Keelson CLI | The latest SDK | Use `keelson dev serve` normally. If the CLI is not installed, or only an older CLI is available, start with `KEELSON_LOCAL_MODE=1`. Your app code is the same either way. ## Start with `keelson dev serve` After `--`, write the command that starts your app's server. ```bash # Node.js keelson dev serve -- node server.js # Python keelson dev serve -- uvicorn main:app --host 127.0.0.1 --port {port} # Go keelson dev serve -- go run . ``` - The CLI picks a free port for your app and passes it in the `PORT` environment variable. It also replaces `{port}` in the command with that number. Your app must listen on that port on `127.0.0.1` - Open `http://localhost:5173/` in your browser. Change the port with `--port` - The CLI sits in front of your app and adds the selected user's headers (`X-Keelson-User-Id` / `X-Keelson-User-Email` / `X-Keelson-User-Name` / `X-Keelson-User-App-Perms`) to every request. As in production, the browser's cookies and `Authorization` header do not reach your app - On startup, the CLI prints the app URL, the current user, and how to switch users. Press Ctrl-C to stop it; your app's server stops too ## Switch users | Method | How to use it | |---|---| | Switch page | Open `http://localhost:5173/__keelson/dev` in your browser and choose a user. Other open tabs keep the previous user until you reload them | | `X-Keelson-Dev-As` header | Add `X-Keelson-Dev-As: ` to a request to act as that user for that request only. Use it when testing with AI agents or `curl` | | `--as` | Run `keelson dev serve --as -- ...` to choose the user at startup | ```bash # Call the API as a user with view permission only curl -H "X-Keelson-Dev-As: local-user-003" http://localhost:5173/api/items ``` An ID that is not in the user list returns an error. ## User list file Provide the list of users as a JSON file. It is looked up in this order: - `keelson dev serve`: `--users ` → `./.keelson/dev-users.json` - `KEELSON_LOCAL_MODE=1`: the `KEELSON_LOCAL_USERS_FILE` environment variable → `./.keelson/dev-users.json` ```json { "users": [ { "id": "sample-alice", "email": "alice@example.com", "name": "Alice Smith", "perms": ["view", "manage"] }, { "id": "sample-bob", "email": "bob@example.com", "name": "Bob Jones", "perms": ["view"] } ] } ``` | Field | Content | |---|---| | `id` | User ID. Must be unique within the list. Any readable string works; it does not need to match production IDs | | `email` / `name` | Email address and display name. Empty strings are allowed | | `perms` | `["view"]` (view only) or `["view", "manage"]` (with manage permission). Becomes the value of `X-Keelson-User-App-Perms` | | `image_url` | Optional. Profile image URL | - The first user in the list with `manage` is the default user - If the specified file is missing or invalid, startup fails with an error - Without a user list file, four built-in users (`local-user-001` to `local-user-004`) are used. With `keelson dev serve`, `local-user-003` and `local-user-004` have view permission only - Restart after changing the user list ## Run without the CLI (`KEELSON_LOCAL_MODE=1`) Start your app's server directly with the `KEELSON_LOCAL_MODE=1` environment variable. ```bash KEELSON_LOCAL_MODE=1 node server.js ``` If your app uses its own user list with `keelson dev serve --users `, also pass `KEELSON_LOCAL_USERS_FILE=` when you start it directly. User IDs then stay the same with or without the CLI. The Identity SDK does not read headers and returns one fixed user (the first user in the list with `manage`) as the current user. Without a user list file, it returns a test user with manage permission (`local-user-001`). You cannot switch users. This test data does not apply to code that reads headers directly. ## Get the current user and permissions (`getRequestUser`) `getRequestUser` returns the current user and their permissions for this app (`perms`) from the request headers in one call. It makes no external request. The same code works in production and in both local modes above. ```javascript import { getRequestUser } from "@keelsonhq/identity"; app.get("/admin", async (req, res) => { const user = await getRequestUser({ headers: req.headers }); // { id, email, name, perms } if (!user.perms.includes("manage")) return res.status(403).end(); res.send(`Hello, ${user.name ?? user.id}`); }); ``` ```python from keelson import identity user = identity.get_request_user(headers=request.headers) if "manage" not in user.perms: ... # Return 403 ``` ```go client, err := identity.New("") // Once at startup // Inside the handler user, err := client.GetRequestUser(identity.WithHeaders(r.Header)) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } canManage := slices.Contains(user.Perms, "manage") ``` A missing user ID header raises an error. `perms` is `X-Keelson-User-App-Perms` split into a list, or empty when the header is absent. Display names in non-ASCII scripts, such as Japanese, are read correctly. ## Not for production These local development features do not work in an app deployed on Keelson. - If `KEELSON_LOCAL_MODE` is set in a deployed app, the SDK raises an error instead of returning test data. Remove `KEELSON_LOCAL_MODE` - `keelson dev serve` also refuses to start in a deployed environment ## Related pages - [Use signed-in user information](https://keelson.dev/docs/building-apps/using-identity/) - [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/) - [CLI reference](https://keelson.dev/docs/reference/cli/#local-development) --- # Use your app from AI clients (MCP) Declare tools in the mcp section of keelson.yaml so AI clients such as Claude and ChatGPT can use your app URL: https://keelson.dev/docs/building-apps/mcp/ Language: en --- ## What you can do You can make your app an **MCP server** (use your app from AI clients). MCP (Model Context Protocol) is a protocol that lets **AI clients** such as Claude and ChatGPT list and call external "tools". For example, if you give an inventory app a "search items" tool and a "record an arrival" tool, users can operate the app from a conversation with their AI client, within their own permissions. Your app only provides the tool declarations in the `mcp:` section of `keelson.yaml` and one JSON endpoint (handler) per tool. Keelson handles login, the consent screen, and the MCP exchange with the AI client. 1. The app author declares tools in the `mcp:` section of `keelson.yaml` and deploys 2. A user registers the **connection URL** (the app URL + `/mcp`) in their AI client 3. The first time, the Keelson login and consent screens appear. When the user approves, the AI client is connected as that user 4. When the AI client calls a tool, Keelson checks membership, app permissions, and IP restrictions, and validates the arguments against the declaration 5. Keelson calls `POST /api/mcp/` on your app and returns the JSON your app sent back to the AI client as the tool result Only apps that run server-side code (apps with a `command` and no `assets`) can use this. Static sites, SPAs, and hybrid apps cannot declare it. It is available on every plan, with no differences between plans. ## Declare tools Add an `mcp:` section to `keelson.yaml` and deploy. The example below declares a read tool, `search_items`, and a write tool, `create_arrival`. ```yaml slug: stock-desk runtime: python-slim command: "python app.py" db: mode: libsql mcp: enabled: true instructions: "Look up stock levels and record incoming deliveries." tools: - name: search_items description: "Search stock by item name." access: read input: type: object properties: query: type: string required: [query] - name: create_arrival description: "Record one incoming delivery." access: write permission: manage input: type: object properties: item_id: type: string quantity: type: integer minimum: 1 required: [item_id, quantity] ``` | Key | Meaning | |---|---| | `enabled` | `true` publishes the tools. `false` keeps the declaration but stops calls | | `instructions` | A description of the tools as a whole, passed to the AI client (optional) | | `tools[].name` | Tool name. Starts with a lowercase letter and uses only lowercase letters, digits, and `_`. Your app's endpoint is `POST /api/mcp/` | | `tools[].description` | The tool description shown to the AI client. The AI reads it to decide when to call the tool | | `tools[].access` | `read` or `write` (changes data). See [Read and write tools, permissions](#read-and-write-tools-permissions) | | `tools[].permission` | `view` (default) or `manage`. A `manage` tool can be used only by people with manage permission on the app | | `tools[].input` | The shape of the arguments (JSON Schema 2020-12). The top level is `type: object` | For the types, limits, and validation of each key, see the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#mcp). ### Put many tools in a separate file (mcp.file) With `mcp.file`, you can put `instructions` and `tools` in a separate YAML file. `keelson.yaml` then contains only `enabled` and `file`. ```yaml slug: stock-desk runtime: python-slim command: "python app.py" db: mode: libsql mcp: enabled: true file: mcp.yaml ``` `mcp.yaml` in the same folder as `keelson.yaml`: ```yaml instructions: "Look up stock levels and record incoming deliveries." tools: - name: search_items description: "Search stock by item name." access: read input: type: object properties: query: type: string required: [query] - name: create_arrival description: "Record one incoming delivery." access: write permission: manage input: type: object properties: item_id: type: string quantity: type: integer minimum: 1 required: [item_id, quantity] ``` - When you deploy, the Keelson CLI inserts the contents of `mcp.yaml` into `keelson.yaml` before validating and uploading it. Your local files are not rewritten - Only `instructions` and `tools` can go in the separate file. `mcp.file` cannot be combined with `mcp.instructions` / `mcp.tools` - Only newer versions of the Keelson CLI read `mcp.file`. Older versions reject the deploy because of an unsupported key. If that happens, run `keelson upgrade` to update the CLI to the latest version ## Write the handlers When a tool is called, Keelson sends `POST /api/mcp/` to your app. The path is determined by the tool name and cannot be changed. The **body** is the JSON object of arguments the AI client passed (`{}` when there are none). Only arguments that passed validation against the declared `input` reach your app. Only **headers** that Keelson builds reach your app. No header sent by the AI client is passed through, so the AI client's access token and forged `X-Keelson-*` headers never reach your app. | Header | Content | |---|---| | `X-Keelson-User-Id` | User ID of the connected user. Use this to identify the user | | `X-Keelson-User-Email` | The same user's email address | | `X-Keelson-User-Name` | The same user's display name | | `X-Keelson-User-App-Perms` | The user's permissions on the app (`view` or `view,manage`) | | `X-Keelson-Auth-Source` | `mcp` (indicates a call through MCP) | | `X-Keelson-Mcp-Tool` | Name of the called tool | | `X-Keelson-Mcp-Client` | Identifier (URL) of the AI client | | `X-Request-Id` | Request ID. Also shown as `request id:` in error results | The `Authorization` header carries a value used inside Keelson. It is not the AI client's token, and your app does not use it. Use the user information headers the same way as in [Use signed-in user information](https://keelson.dev/docs/building-apps/using-identity/). Return a **response** of 2xx with JSON (up to 1 MiB). Returning a JSON object makes the fields easier for the AI client to read. | Your app's response | Result returned to the AI client | |---|---| | 2xx with a JSON body | The JSON body as the tool result | | 2xx with an empty body, a non-JSON body, or a body over 1 MiB | Error (invalid tool response) | | 4xx | Error. The first 4 KiB of the body is shown as is to the user and the AI client | | 3xx or 5xx | Error (tool execution failed). The body is not shown | | No response within 60 seconds | Error (unknown whether it ran) | - **The body of a 4xx response is visible to the user and the AI client.** Write only explanations that may be shown (for example, "Item A-100 was not found") - Each call has **60 seconds** in total. Enqueue slow work as a [background task](https://keelson.dev/docs/building-apps/background-tasks/) and respond right away - Keelson does not retry failed calls ### FastAPI example ```python from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app = FastAPI() @app.post("/api/mcp/search_items") async def search_items(request: Request): user_id = request.headers["X-Keelson-User-Id"] args = await request.json() # e.g. {"query": "bolt"} items = find_items(owner=user_id, query=args["query"]) # your app's search return {"items": items} @app.post("/api/mcp/create_arrival") async def create_arrival(request: Request): perms = request.headers.get("X-Keelson-User-App-Perms", "").split(",") if "manage" not in perms: return JSONResponse({"error": "You do not have permission to record arrivals"}, status_code=403) args = await request.json() if not item_exists(args["item_id"]): # The body of a 4xx response is visible to the user return JSONResponse({"error": f"Item {args['item_id']} was not found"}, status_code=404) arrival_id = record_arrival(args["item_id"], args["quantity"]) return {"arrival_id": arrival_id} ``` ### Express example ```javascript app.use(express.json()); app.post("/api/mcp/search_items", (req, res) => { const userId = req.headers["x-keelson-user-id"]; const items = findItems({ owner: userId, query: req.body.query }); // your app's search res.json({ items }); }); app.post("/api/mcp/create_arrival", (req, res) => { const perms = (req.headers["x-keelson-user-app-perms"] ?? "").split(","); if (!perms.includes("manage")) { return res.status(403).json({ error: "You do not have permission to record arrivals" }); } const { item_id, quantity } = req.body; if (!itemExists(item_id)) { // The body of a 4xx response is visible to the user return res.status(404).json({ error: `Item ${item_id} was not found` }); } res.json({ arrival_id: recordArrival(item_id, quantity) }); }); ``` Keelson does not pass calls to a `permission: manage` tool from people without manage permission. Checking `X-Keelson-User-App-Perms` in your app as well, as in the examples above, keeps the tool safe if the declaration changes. ## Read and write tools, permissions - **Tools with `access: write` are shown separately on the consent screen as "Write tools (can change data)".** Users can choose "Approve read tools only". In that case, write tools cannot be used. To use them later, the user connects again and approves them - **Tools with `permission: manage` can be used only by people with manage permission on the app.** For assigning permissions, see [Members and roles](https://keelson.dev/docs/access-control/members-and-permissions/) - **Tools a user cannot use do not appear in the list.** Tools that need permissions the user lacks, and write tools the user did not approve, are removed from the AI client's tool list - Every call is made as the connected user, within that user's permissions. Calls from people removed from the workspace or who lost view permission on the app are usually rejected within 2 minutes ## Connect Users register the **connection URL** in their AI client. The connection URL is the app URL followed by `/mcp` (for example, `https://acme--stock-desk.keelson.run/mcp`), and it is also shown on the app's **MCP** tab in the console. For how to register the connection URL, see the MCP settings documentation of each AI client. 1. Register the connection URL in the AI client 2. The first time, the Keelson login screen opens. Log in with an account that can use the app 3. On the consent screen, check the tools the AI client will use and select **Approve** - Only members who can view the app can connect - Only AI clients that identify themselves as `claude.ai` or `chatgpt.com` are accepted. Other clients stop at the login step even if the connection URL is registered - Access tokens are valid for 1 hour, and the AI client renews them automatically. The renewal token expires after 60 days without use, or at most 180 days after connecting. When it expires, connect again - Connecting the same AI client to the same app again disconnects the previous connection ## Stop and disconnect | Action | Who | Effect | |---|---|---| | Stop MCP for the workspace (**Settings** → **Security** → **MCP (AI clients)**) | Owner / Admin | Stops calls to every app in the workspace. Connections remain and work again once MCP is allowed | | Deploy with `mcp.enabled: false` | People who can deploy the app | Stops calls to that app. Connections remain and work again after you set it back to `true` and deploy | | **Revoke** under "Connected AI clients" on the app's **MCP** tab | Owner / Admin, people with manage permission on the app | Disconnects that connection. The AI client immediately loses access, including its renewal token | | **Revoke** under "Your connected AI clients" | The user who connected | Disconnects the user's own connection | Stopping MCP and `mcp.enabled: false` only stop calls. **Only disconnecting (revoking) ends a connection.** "Your connected AI clients" is linked from **Settings** → **Security** and from the consent screen. ## Reserved paths - For an app with `mcp.enabled: true`, Keelson responds to `/mcp`, `/.well-known/oauth-protected-resource`, and `/.well-known/oauth-protected-resource/mcp`, and they never reach your app - For apps that run server-side code (including the server side of a hybrid app), paths `/api/mcp` and paths starting with `/api/mcp/` return 404 except through MCP, even without an `mcp:` declaration. Do not put routes other than tool handlers on these paths. Statically served parts are not affected For the list of reserved paths, see [Reserved URL paths in the Keelson Deploy Spec](https://keelson.dev/docs/reference/deploy-spec/#reserved-url-paths). ## Limits | Item | Limit | |---|---| | Number of tools | 40 per app | | `instructions` | 4096 characters | | Tool `description` | 1024 characters | | Tool `input` | 64 KB as JSON, 32 levels of nesting | | One call | 60 seconds | | Response body (2xx) | 1 MiB | | Calls | 60 per minute per connection | | Concurrent calls | 10 per app (per Keelson gateway instance; when the gateway scales out, your app can receive more concurrent calls than this in total) | Calls over the rate or concurrency limit are returned to the AI client with a request to retry after a short wait. Limits do not change by plan. The concurrency limit of 10 is not a ceiling on the concurrent calls your app receives, so do not size database connection pools or similar resources on the assumption of 10. ## Troubleshooting | Symptom | What to check | |---|---| | A tool does not appear in the list | Whether the user has permission on the app (`permission: manage` tools need manage permission). For a write tool, whether the user approved it on the consent screen. Whether `mcp.enabled` is `true`. Whether MCP is stopped for the workspace (shown in the status on the app's **MCP** tab) | | Calls are rejected in a workspace with IP restrictions | IP restrictions also apply to calls through MCP. Calls from claude.ai or ChatGPT come from each provider's servers, not from the user's computer, so they are not allowed as is. Review the access sources in [IP restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) | | "Invalid tool response" is shown | Whether the handler returns 2xx with a JSON body, and the body is not empty, plain text, or over 1 MiB | | Cannot connect after logging in | Whether the AI client identifies itself as `claude.ai` or `chatgpt.com`. Whether the account you logged in with is a member who can view the app | | Deploy rejects `file` under `mcp` as an unsupported key | The Keelson CLI is old. Run `keelson upgrade` to update to the latest version, then deploy again | ## Related pages - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#mcp) - [Use signed-in user information](https://keelson.dev/docs/building-apps/using-identity/) - [IP restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) - [Members and roles](https://keelson.dev/docs/access-control/members-and-permissions/) --- # External integrations Receive webhooks, use app API tokens, send email, and connect to external databases URL: https://keelson.dev/docs/building-apps/external-integrations/ Language: en --- Choose the integration method that fits your task: call an API such as OpenAI, receive notifications from another service, or connect to an external database. | What you want to do | Setup | |---|---| | Call an external API | [Set connection details and API keys](#call-an-external-api) | | Send email to the app's users | [Send with the Email SDK](#send-email) | | Receive notifications from an external service | [Create a webhook app token and configure a receiving endpoint](#receive-webhooks) | | Call the app's API from another internal system | [Create an app token and declare allowed paths](#call-your-apps-api-from-another-system) | | Use an external database or storage service | [Set connection details](#external-databases) | ## Call an external API Outbound connections from apps to external services are unrestricted, with one exception: the SMTP ports (TCP 25, 465, 587, and 2525) are always blocked. Set required API keys as [secrets](https://keelson.dev/docs/deploy/environment-variables/) and call the service from your app. The external service's contract and rate limits still apply. To send email, use Keelson's [Email SDK](#send-email) or the HTTPS API of an email service you contract with, such as Resend, SendGrid, or Mailgun. Contracted services cannot be used through their SMTP relay hosts either, so configure the API client rather than an SMTP host and port. Calls in the other direction—from an external system to your app—require Keelson login by default. For systems that cannot sign in through a browser, use the setup below. ## Receive webhooks Webhooks notify your app about events in another service. For example, a payment notification can trigger an update to an order's status. Receiving a webhook requires an app token with the `webhook` scope, even if the sender also signs its requests. Declaring `auth.endpoints` alone does not allow requests authenticated only by the sender's signature. First, issue a token for the target app. Replace `my-app` with its slug. ```bash keelson apps tokens create --app my-app --name webhook-client --scope webhook ``` Implement a receiving handler such as `POST /api/webhooks/incoming`. Append one of the following paths to your app URL, for example `https://acme--my-app.keelson.run`, and register it with the sender. | What the sender can configure | Setup | |---|---| | Custom HTTP headers | Use `/api/webhooks/incoming` as the receiving path and add `X-Webhook-Secret: ` | | Receiving URL only | Use `/api/webhooks//incoming` as the receiving path | When the token is in the URL, Keelson removes that segment and forwards the request to the app's `/api/webhooks/incoming`. Treat URLs containing tokens as secrets. These requests do not include signed-in user information. Keelson checks token validity, the target app, the `webhook` scope, and source IP restrictions. Currently, `auth.endpoints` cannot restrict webhook paths or methods. Check allowed paths and methods in your receiving handler. If the sender signs requests, receive its signing secret through `secrets` and verify the signature in your app. The Keelson app token and the sender's signing secret are separate credentials. `/api/webhooks/email` and `/api/webhooks/email-events` are reserved by the platform and cannot be used for your own webhooks. Do not add them to `auth.endpoints`. ## Call your app's API from another system Issue an app token to call the app's API from another internal system or a script. ```bash keelson apps tokens create --app my-app --name batch-client --scope api --allowed-ip 203.0.113.0/24 ``` Replace `my-app` with the target app's slug and `203.0.113.0/24` with the actual source IP range. - Tokens have the form `keelson_...` and are displayed only once, at creation. - `--scope api` applies under `/api/external/`; `--scope webhook` applies under `/api/webhooks/`. - `--allowed-ip` restricts source IPs using CIDR ranges; multiple ranges are allowed. These restrictions apply per token, separately from workspace or app IP controls. - You can configure the same settings on the app's console page. Under `keelson apps tokens`, use `list` to list tokens, `rotate` to replace one, or `delete` to revoke one. The caller adds `Authorization: Bearer keelson_...` and calls a path declared in `auth.endpoints`. ```bash curl -H "Authorization: Bearer keelson_xxx" https://acme--my-app.keelson.run/api/external/status ``` ```yaml auth: endpoints: - path: /api/external/status methods: [GET] - /api/external/import # No methods specified means all methods ``` Before forwarding the request, Keelson checks that the token is valid for the app and purpose, the source IP is allowed, and the path and method match the declaration. Paths in `auth.endpoints` use prefix matching, not exact matching. For example, `/api/external/status` also matches `/api/external/status/detail` and `/api/external/status-old`. Check allowed paths and methods in the app too. An `api`-scoped token cannot call undeclared APIs. ## Send email With Keelson's Email SDK, your app can send email to its users without contracting an email service. Use it for business messages such as password resets, request confirmations, and approval requests. Email may only be sent as business communication to that app's users ([Acceptable Use Policy](https://keelson.dev/aup/) 2.3). Marketing campaigns, sending to purchased address lists, and sending to people who do not use the app are not allowed. Recurring notifications must give recipients a way to stop receiving them. ```ts import * as email from "@keelsonhq/email"; const result = await email.send({ to: "user@example.com", subject: "Request received", text: "We received request 1234.", }); // Store result.send_id to match it with delivery results later ``` In Python, import it with `from keelson import email` and call `email.send(...)`. In Go, create a client with `email.New("", "")` from `github.com/keelsonhq/go-sdk/email` and call `Send`. For installation, see [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/#installation). - Keelson sets the endpoint and token (such as `KEELSON_EMAIL_TOKEN`) at deploy time, and the SDK reads them. No `keelson.yaml` setting is needed. Static sites and SPAs have no server to run the SDK, so they cannot send email - The sender is `@mail.keelson.run` by default. You can set the display name (`from_name`) and reply-to address (`reply_to`). On the Team plan, you can also send from an allowed address on your own domain ([see below](#send-from-your-own-domain)) - A message can have up to 50 recipients across To, CC, and BCC. The body needs at least one of `text` or `html` - Sends are limited to 30 per 60 seconds, per app and per workspace - The number of recipients per month depends on the plan ([Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/)). Check this month's usage under Email in the console's workspace settings ### Send from your own domain On the Team plan, apps can send from an address on a domain your workspace owns, such as `noreply@example.com`. Each workspace can register up to 10 domains, and each domain can have up to 20 allowed sender addresses. Owners and Admins set this up under Email > Domains in the console's workspace settings. You need permission to change the domain's DNS settings. 1. Under **Add Domain**, enter the domain. To limit the domain to one app, choose the app in **Scope**; otherwise, every app in the workspace can use it. 2. Add the TXT record shown under **Ownership check** to your DNS, then select **Check status**. Only one workspace can verify a given domain. 3. After ownership is confirmed, three CNAME records for email signing (DKIM) appear. Add them to your DNS. Verification usually completes within minutes. Select **Check status** to see the current status. If the records are not found within 72 hours, verification fails; remove the domain and add it again. 4. Under **Allowed sender addresses**, add each address your apps send from. 5. In your app, pass the address as `from_address` (Node and Python) or `From` (Go). ```ts await email.send({ from_address: "noreply@example.com", from_name: "Example Support", to: "user@example.com", subject: "Request received", text: "We received request 1234.", }); ``` - Keelson checks the From address on every send. A send is refused unless the address exactly matches an allowed address (letters are not case-sensitive) on a verified domain of the sending app's workspace. Removing an allowed address takes effect from the next send - Email is signed with DKIM for your domain, so the signature is aligned with the From address for DMARC. Keelson does not use your domain for the return path (SPF), so SPF is not aligned - The default `@mail.keelson.run` sender needs no allowed addresses and keeps working - If the workspace leaves the Team plan, sends from your own domain are refused. Registered domains and allowed addresses are kept and work again after you return to Team - Bounces, complaints, and the suppression list work the same as for the default sender ### When a send fails If a send is rejected, the SDK throws an error. The error message includes the error code. | HTTP | Error code | Meaning | |---|---|---| | 400 | `RECIPIENT_SUPPRESSED` | A recipient is on the workspace suppression list ([see below](#receive-email-delivery-results)) | | 403 | `EMAIL_SENDING_SUSPENDED` | Email sending is suspended for the workspace. Too many permanent bounces or complaints automatically suspend sending for the whole workspace. [Contact us](https://keelson.dev/contact/) to lift the suspension | | 403 | `SENDER_DOMAIN_NOT_AVAILABLE` | The From address is on your own domain, but the workspace is not on the Team plan | | 403 | `DOMAIN_NOT_VERIFIED` | The From domain is not verified for the sending app's workspace, or the domain is limited to another app | | 403 | `SENDER_ADDRESS_NOT_ALLOWED` | The From address is not an allowed sender address for its domain | | 422 | `RECIPIENT_LIMIT_EXCEEDED` | More than 50 recipients in total | | 429 | `RATE_LIMIT_EXCEEDED` | The 60-second send limit was reached. Retry later | | 429 | `MONTHLY_QUOTA_EXCEEDED` | This month's recipient limit was reached. It resets next month | | 429 | `GLOBAL_RATE_LIMIT_EXCEEDED` / `GLOBAL_DAILY_QUOTA_EXCEEDED` | Keelson's overall sending volume temporarily reached its limit. Retry later | | 502 | `SEND_OUTCOME_UNKNOWN` | Keelson could not confirm whether the message was sent. It may have been delivered, so do not retry automatically | ## Receive email delivery results Keelson notifies the app's `POST /api/webhooks/email-events` when a message is delivered, returned because the address does not exist or for another reason (bounce), or reported as spam by the recipient (complaint). To use these results, implement a handler at this path. No app token or `auth.endpoints` setting is needed. Recipients with a permanent bounce or a complaint are added to the workspace suppression list automatically, whether or not the app handles the notification. After that, no app in the same workspace can send to that address (`RECIPIENT_SUPPRESSED`). Owners and Admins can review and remove addresses under Email in the console's workspace settings. Notifications are signed. Verify the signature with the SDK before using the content. Keelson sets the signing key, `KEELSON_EMAIL_WEBHOOK_SECRET`, automatically. ```ts import * as email from "@keelsonhq/email"; // Handler for POST /api/webhooks/email-events. rawBody is the request body as received const event = email.verifyEventWebhookBytes( rawBody, req.headers, process.env.KEELSON_EMAIL_WEBHOOK_SECRET!, ); if (event.event_type === "bounce" && event.bounce_type === "hard") { // Look up the send record by event.send_id and mark the address invalid } ``` In Python, use `email.verify_event_webhook(body, headers, secret)`. In Go, use `email.VerifyEventWebhook(r, secret)`. The signature covers the raw body bytes, so pass the body before parsing it as JSON. | Field | Content | |---|---| | `event_id` | Notification ID. The same notification can arrive more than once, so use it to detect duplicates | | `event_type` | `delivered`, `bounce`, or `complaint` | | `email_address` | The recipient address | | `send_id` | The `send_id` returned when the message was sent. Use it to match the notification with your send record | | `bounce_type` | For bounces, `hard` (permanent) or `soft` (temporary) | | `detail` | Additional information | | `provider` | Name of the sending infrastructure used for delivery | | `timestamp` | When the event occurred | A 2xx response completes the notification. If the handler returns 5xx or the notification cannot reach the app, Keelson retries later, up to 4 attempts in total. Notifications that receive a 4xx response are not retried. ## External databases To connect to PostgreSQL, MySQL, or your own libSQL instance, set `db.mode: none` and pass connection details through secrets. See [Use an external database](https://keelson.dev/docs/building-apps/persistent-storage/#use-an-external-database). ## External object storage For files that exceed the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) limits, write directly to S3-compatible storage from your app. Pass credentials through secrets. ## Related pages - [Use signed-in user information](https://keelson.dev/docs/building-apps/using-identity/) - [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#auth) --- # Receive email Give your app an email address, store incoming mail in its database, and process it with a background task URL: https://keelson.dev/docs/building-apps/receive-email/ Language: en --- Your app can have its own email address. Keelson stores every email that arrives as one row in a table in the app's database and saves its attachments in Files. Your app reads them with SQL; it does not need code that receives mail. To process each email after it is saved, name a [background task](https://keelson.dev/docs/building-apps/background-tasks/) in `on_receive`. Typical uses are a shared support inbox and reading order or invoice emails. Keelson does not receive mail for your own domain. Forward mail to the app's address from Gmail, Google Workspace, or Microsoft 365 instead ([Forward mail to the app](#forward-mail-to-the-app)). Tell your AI agent what should happen to each email, and it can write the `keelson.yaml` settings and the processing task. > Store the order emails forwarded from our sales@ group and register each one as an order. Processing the same email twice must not create a duplicate order. ## Enable receiving Add `email.inbound` to `keelson.yaml` and deploy. Receiving needs Managed SQLite (`db.mode: libsql`). ```yaml slug: order-inbox runtime: python-slim command: "uvicorn main:app --host 0.0.0.0 --port $PORT" db: mode: libsql # required email: inbound: enabled: true on_receive: process-mail # optional: a task declared under tasks tasks: - name: process-mail command: "python process_mail.py" ``` | Field | Meaning | |---|---| | `email.inbound.enabled` | `true` turns receiving on. The deploy issues the receiving address and creates the table `keelson_inbound_emails` | | `email.inbound.on_receive` | Optional. The name of a task under `tasks`. Keelson starts it after each email is saved | The deploy is rejected in these cases: | Error | Cause | |---|---| | `email_inbound_requires_libsql` | `enabled: true` without `db.mode: libsql` | | `email_on_receive_requires_enabled` | `on_receive` is set but `enabled` is not `true` | | `email_on_receive_unknown_task` | `on_receive` does not match any `tasks[].name` | With `on_receive`, an app that has `tasks` needs neither `command` nor `crons`; receiving is what starts the task. The location of attachments is fixed and cannot be configured. ## Receiving address The receiving address appears on the app's Email settings page in the console. It has the form `in+@inbound.keelson.run`. The random part is not derived from the slug, so renaming the app does not change the address. - **Rotate**: you can issue a new address from the console. The old address is retired immediately, and mail sent to it is refused. - **Turn receiving off**: deploying without `email.inbound` (or with `enabled: false`) retires the address permanently. Emails not yet saved, or held because they could not be saved, are discarded. Rows and attachments already saved stay in place. - **Turn it on again**: a new address is issued. Update the forwarding settings to the new address. A retired address is never reused, by your app or any other. ## How emails are stored Each email becomes one row in `keelson_inbound_emails`. The table is in the app's own database, so the app reads it with the same connection as its other tables. | Column | Contents | |---|---| | `email_id` | Primary key. Fixed when the email is received | | `schema_version` | Version of the column layout when the row was written. Currently `1` | | `received_at` | Time received, ISO 8601 UTC | | `envelope_to` | The app's receiving address the email was delivered to | | `envelope_from` | The sender address used for delivery (the envelope sender) | | `from_address` | The sender's address: `X-Original-From` if present, otherwise `From`. **Not authenticated** ([below](#the-sender-is-not-authenticated)) | | `from_name` | The sender's display name, from the same header | | `header_from` | The raw `From` header, after any rewriting by a forwarding group | | `reply_to` | `Reply-To` header | | `to_addresses` | JSON array of the `To` addresses (for forwarded mail, your shared inbox) | | `cc_addresses` | JSON array of the `Cc` addresses | | `subject` | Subject | | `message_id` | `Message-ID` header | | `in_reply_to` | `In-Reply-To` header | | `references_header` | `References` header | | `list_id` | `List-Id` header | | `text_body` | Plain-text body, decoded to UTF-8 | | `html_body` | HTML body, decoded to UTF-8 | | `body_truncated` | `1` if either body was longer than 1 MiB and was cut | | `attachments` | JSON array of attachments (`[]` if none) | | `spam_score` | Simple spam score. A marker only ([Spam](#spam)) | | `auth_spf` / `auth_dkim` / `auth_dmarc` | SPF / DKIM / DMARC results recorded by the receiving server | Headers that are missing are `NULL`. Encoded subjects and file names (including Japanese) are decoded. `to_addresses` and `cc_addresses` are arrays of addresses (`name` is `null` when absent): ```json [{"address": "support@example.com", "name": "Support"}] ``` Each item in `attachments`: ```json [{"attachment_id": "att_01", "filename": "order.pdf", "content_type": "application/pdf", "content_id": null, "size_bytes": 182344, "files_key": "inbound//att_01", "sha256": ""}] ``` `content_id` is set for inline images referenced from the HTML body. ### Read emails with SQL ```sql -- Latest 20 emails SELECT email_id, received_at, from_address, subject FROM keelson_inbound_emails ORDER BY received_at DESC LIMIT 20; -- Attachments of one email SELECT json_extract(a.value, '$.filename') AS filename, json_extract(a.value, '$.files_key') AS files_key FROM keelson_inbound_emails e, json_each(e.attachments) a WHERE e.email_id = ?; ``` ### Read attachments Attachments are saved in [Files](https://keelson.dev/docs/building-apps/files-and-media/) under the key `inbound//`, which is the `files_key` in `attachments`. Read them with the Files SDK. ```python import json from keelson import files (attachments,) = conn.execute( "SELECT attachments FROM keelson_inbound_emails WHERE email_id = ?", (email_id,) ).fetchone() for att in json.loads(attachments): data = files.read(att["files_key"]) # bytes | None ``` ```javascript import * as files from "@keelsonhq/files"; const { rows } = await db.execute({ sql: "SELECT attachments FROM keelson_inbound_emails WHERE email_id = ?", args: [emailId], }); for (const att of JSON.parse(rows[0].attachments)) { const data = await files.read(att.files_key); // Uint8Array | null } ``` An attachment can be larger than the Files write limit (10 MiB). Reading it works. Attachments count toward the workspace storage; the rows do not. ### Rules for the table - Keelson creates `keelson_inbound_emails` on the deploy that turns receiving on. Do not create it yourself in migrations or app code. If a table with the same name already exists and Keelson did not create it, the deploy stops. - The table is yours: the app can read, update, and delete rows. Keep your own state, such as "handled" or "assignee," in a separate table keyed by `email_id`. - Keelson only inserts rows. It never rewrites existing rows or recreates the table. - Do not remove the `email_id` primary key, drop columns, or add triggers to the table, and do not save your own files under the Files key `inbound/`. New emails then cannot be saved and are held ([Emails that could not be saved](#emails-that-could-not-be-saved)). - Restoring the database from a backup also restores this table to that point. ## Process each email (`on_receive`) When `on_receive` names a task, Keelson starts that task after each email is saved. The task's `payload` is the email ID only: ```json {"attempt_no":1,"payload":{"email_id":"…"},"schema_version":1,"task_id":"…","task_name":"process-mail"} ``` The task reads the row from `keelson_inbound_emails` by `email_id`. Other than how it is started, it is an ordinary background task ([Background tasks](https://keelson.dev/docs/building-apps/background-tasks/)). **The task is started at least once.** The same `email_id` can be passed more than once, and failed attempts are retried. Record which emails you have handled, and write the record and the result in one transaction: ```python import json import os import sys import libsql email_id = json.loads(sys.stdin.readline())["payload"]["email_id"] conn = libsql.connect( database=os.environ.get("KEELSON_DB_URL", "file:local.db"), auth_token=os.environ.get("KEELSON_DB_AUTH_TOKEN", ""), ) conn.execute( "CREATE TABLE IF NOT EXISTS handled_emails (email_id TEXT PRIMARY KEY, handled_at TEXT NOT NULL)" ) cur = conn.execute( "INSERT INTO handled_emails (email_id, handled_at) VALUES (?, datetime('now')) " "ON CONFLICT (email_id) DO NOTHING", (email_id,), ) if cur.rowcount == 0: sys.exit(0) # already handled (subject,) = conn.execute( "SELECT subject FROM keelson_inbound_emails WHERE email_id = ?", (email_id,), ).fetchone() conn.execute("INSERT INTO orders (email_id, title) VALUES (?, ?)", (email_id, subject)) conn.commit() # the handled record and the order are saved together ``` If the task fails before the commit, nothing is saved and the retry handles the email from the start. For external APIs, pass `email_id` as the idempotency key. - From the console you can start `on_receive` again for a saved email. Each start counts the same as an automatic one. - Tasks started by `on_receive` do not count toward Background Jobs runs. Their run time counts toward the monthly Background Jobs runtime. If the runtime allowance is used up, the task is not started; the console shows this, and it is not retried automatically. - Heavy processing can use up the runtime allowance and stop scheduled jobs too. Check the monthly usage in the console. ## The sender is not authenticated `from_address` comes from the `X-Original-From` or `From` header. Anyone can write any address there. **Do not use `from_address` to identify a user or to decide what an email is allowed to do**, for example "emails from the CEO approve a payment." Use it for display and for sorting mail only. `auth_spf`, `auth_dkim`, and `auth_dmarc` record checks made by the receiving server. Forwarding often breaks them, so they do not prove who sent the email either. ## Forward mail to the app Keelson does not receive mail for your own domain. Keep receiving mail where you receive it now and forward it to the app's address. ### Gmail 1. In Gmail, open **Settings** → **See all settings** → **Forwarding and POP/IMAP** and add the app's receiving address as a forwarding address. 2. Gmail sends a confirmation email from `forwarding-noreply@google.com` to that address. Open **Recent emails** on the app's Email settings page in the console and read the confirmation code. This works even when the app is not running. 3. Enter the code in Gmail and turn on forwarding. To forward only some emails, create a Gmail filter that forwards matching messages. The confirmation email is also stored in the table like any other email. If you use `on_receive`, the task is started for it too. ### Google Workspace (groups) Add the app's receiving address as a member of the Google Group that receives the mail (for example, support@). The group then delivers a copy of each email to the app. - If the address cannot be added, your Google Workspace administrator needs to allow groups to have members outside the organization. - When the original sender's domain has a strict DMARC policy, Google Groups rewrites `From` to the group's address and keeps the original sender in `X-Original-From`. `from_address` uses `X-Original-From`, so it holds the original sender; `header_from` holds the rewritten `From`. `to_addresses` holds the group address. ### Microsoft 365 Microsoft 365 blocks automatic forwarding to external addresses by default (the outbound spam filter policy). An administrator must allow it before forwarding works. Otherwise forwarded mail bounces with `550 5.7.520 Access denied, Your organization does not allow external forwarding`. 1. In the Microsoft Defender portal, open the outbound anti-spam policy and set automatic forwarding to on. Prefer a custom policy that applies only to the mailbox that forwards. 2. In that mailbox, set up forwarding to the app's address (Outlook forwarding settings or an inbox rule), or use a mail flow rule. Keelson has not tested forwarding from Microsoft 365. These steps are based on Microsoft's documentation. ## Limits | Item | Limit | |---|---| | Size of one email | 25 MiB, including attachments. Larger emails are refused | | Body | `text_body` and `html_body` are each cut at 1 MiB (`body_truncated = 1`) | | Attachments | No limit per attachment within the 25 MiB | | Emails received per month | Per workspace, all apps combined. See below | **Monthly receiving limit** (per workspace, calendar month in UTC): | | Starter | Plus | Team | Business | |---|---|---|---|---| | Emails received per month | 500 | 3,000 | 10,000 | 30,000 | An email is counted once, after duplicates are removed. Workspace Owners and Admins are notified at 80%, 100%, and 150%. - **Above 100%**: emails are still received and stored, but `on_receive` is not started for them. The console marks those emails. After upgrading your plan, you can start `on_receive` for them from the console. - **At 150%**: the workspace's addresses refuse mail, and senders get a bounce. Receiving resumes at 00:00 UTC on the 1st of the next month or after a plan change. A few emails that arrive while this takes effect may still be stored. ## Emails that could not be saved If an email cannot be written to the table, for example because the table was dropped, its primary key was removed, a trigger rejected the row, or a different file already exists under `inbound/`, Keelson holds the email. The console's email history shows the reason and how to fix it. After fixing the table, retry the email from the console. - Held emails are kept for 7 days, up to 1,000 per app. While the app is at 1,000 held emails, new mail to its address is refused. - After 7 days a held email is recorded as expired, and the email and its attachments are deleted. The email history in the console also shows emails waiting to be saved, emails that could not be read as email at all, and emails discarded because receiving was turned off. ## Spam `spam_score` is a simple score. Keelson does not quarantine or delete mail based on it, and every email is stored. Forwarded mail often fails SPF, so ordinary mail can get a score too. Decide in your app what to do with high scores. ## If mail does not arrive 1. **Check the console.** Open the email history on the app's Email settings page. If the email is there, Keelson received it; check its state (held, and why). 2. **Check the address.** Mail to a rotated address, or to an app with receiving turned off, is refused. 3. **Check the limits.** At 150% of the monthly limit, or with 1,000 held emails, new mail is refused. Emails over 25 MiB are refused. 4. **Check the forwarding.** Gmail forwarding starts only after you enter the confirmation code. Microsoft 365 blocks external forwarding unless an administrator allows it. 5. **Authentication checks before Keelson.** Mail that fails both SPF and DKIM, or that fails the sender domain's DMARC policy, is rejected by the receiving service before it reaches Keelson. Such mail leaves no record in Keelson, and the sender usually gets a bounce. Forwarding that changes the message can cause this. Forwarding through a Google Group delivers mail from such senders. ## Test locally `keelson dev email` stores a saved email file (`.eml`) in your local database and Files, the same way Keelson stores received mail. ```bash export KEELSON_DB_URL=file:local.db keelson dev email ./samples/order.eml ``` ```text keelson dev email [--to
] ``` - The message is parsed with the same mapping as on Keelson and inserted as one row into `keelson_inbound_emails` in the database at `KEELSON_DB_URL`. If the table does not exist, it is created (locally only). - Attachments are written under `KEELSON_FILES_DIR` (default `./.keelson/files`) at `inbound//`, where the Files SDK reads them in local development. - `KEELSON_DB_URL` must point to your machine: a `file:` URL, or an `http://`, `libsql://`, or `ws://` URL whose host is `127.0.0.1` or `localhost`. Otherwise, or if it is unset, the command exits with code 2 without writing anything. - `--to` sets the receiving address (`envelope_to`). The default is `in+localdev@inbound.keelson.run`. - The output shows the `email_id`, the subject, the number of attachments, and where they were written. - `email_id` is derived from the message contents and `--to`. Running the same file again prints `Already stored` and exits with code 0. - A file that cannot be parsed as an email exits with code 1. - `on_receive` is not started. If `keelson.yaml` declares it, the command prints how to run the task: ```bash keelson dev task run process-mail --payload '{"email_id":"…"}' ``` To get an `.eml` file, download the original message from your mail client (in Gmail, **Show original** → **Download original**). ## Related pages - [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) - [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) - [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) --- # Framework notes Production startup settings for FastAPI, Flask, Express, Next.js, and Go, and notes on unsupported frameworks URL: https://keelson.dev/docs/building-apps/frameworks/ Language: en --- This page covers production startup commands and considerations for each framework. Start your web server on the port specified by the `PORT` environment variable. Some frameworks have database or connection constraints, so also check [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/). Use a production server. Development servers can have performance issues, expose internal information in error pages, and handle in-flight requests poorly during shutdown. Your AI agent can usually handle configuration. Refer to the section for your framework when checking startup settings or investigating deployment problems. ## Settings shared by server-side apps - **Listen on `0.0.0.0` and the port specified by `PORT`.** Requests cannot reach a server bound to `127.0.0.1`. - **Do not install dependencies in the startup command.** Dependencies are installed at build time from `requirements.txt`, `pyproject.toml`, `package.json`, `go.mod`, or other supported files. - **Shutdown sends SIGTERM, then SIGKILL after 10 seconds.** Finish in-flight requests within 10 seconds. - **Start with one worker.** If you add workers, account for available CPU, memory, and the work your app performs. - **Disable debug mode by default.** Debug pages may expose environment variables, including database authentication tokens. - **Use `KEELSON_MODE` to detect Keelson.** A value of `keelson` means the app is on the platform. Keep environment detection separate from enabling or disabling debug mode. ```python import os ON_KEELSON = os.environ.get("KEELSON_MODE") == "keelson" DEBUG = os.environ.get("DEBUG", "false").lower() == "true" # Defaults to false ``` ## Python ### FastAPI / uvicorn ```yaml command: "uvicorn main:app --host 0.0.0.0 --port $PORT" env: PYTHONUNBUFFERED: "1" ``` Do not add `--reload`: file watching consumes memory and can start the app twice. Keep the default of one worker. ### Flask `app.run()` starts a development server. Add gunicorn to `requirements.txt` and use it to start the app. ```yaml command: "gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 --graceful-timeout 9 app:app" env: PYTHONUNBUFFERED: "1" ``` Replace `app:app` with your module name and app object name. `--graceful-timeout 9` keeps graceful shutdown within the 10-second SIGTERM window. ### Django Django deploys only when it connects to an external database such as PostgreSQL. Managed SQLite (libSQL) has no backend for the Django ORM, so configurations that use Managed SQLite or a SQLite file as the database are not supported. Set `db.mode: none` and pass the connection details as [secrets](https://keelson.dev/docs/deploy/environment-variables/). Start with gunicorn, not `runserver`. ```yaml command: "gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 --graceful-timeout 9 config.wsgi:application" db: mode: none secrets: items: - name: DATABASE_URL ``` Replace `config.wsgi:application` with your project's WSGI module. Keep `DEBUG` off by default, serve static files with whitenoise (set `STATIC_ROOT` and run `collectstatic` in the build), and keep `ALLOWED_HOSTS = ["*"]`. See the [Deploy Spec](https://keelson.dev/docs/reference/deploy-spec/#production-server-by-framework) for details. ### SQLAlchemy / SQLModel / Flask-SQLAlchemy Apps using file-based SQLite may be able to connect to Managed SQLite with `sqlalchemy-libsql-native`. This approach is experimental. Beyond changing the connection settings, verify that models, queries, and migrations work. Ask your AI agent to review the required changes and compatibility. ### Streamlit / Gradio Streamlit currently does not work because it requires WebSocket. Gradio 4 and later are being evaluated. ## Node.js ### Shared settings `NODE_ENV` is not set automatically. Declare it in `keelson.yaml`. Without it, frameworks such as Express may include stack traces in responses. ```yaml env: NODE_ENV: "production" ``` Keelson uses npm for builds on the platform. If your project only has `pnpm-lock.yaml` or `yarn.lock`, preflight checks stop the deployment. Generate `package-lock.json` and verify that installation and building succeed with npm. Static sites built locally with pnpm or Yarn can still be served. See [Dependency installation](https://keelson.dev/docs/reference/keelson-yaml-reference/#dependency-installation). ### Express ```yaml command: "npm start" # package.json: "start": "node server.js" ``` - Do not return stack traces from error handlers. - If you use `trust proxy` to determine the client IP or whether a request uses HTTPS, configure it for the actual proxy path and forwarded headers. A fixed value of `1` may not work correctly ([official Express guide](https://expressjs.com/en/guide/behind-proxies/)). - Declare secrets for your app's own sessions or JWTs in `secrets`. ### Next.js Build the app and start it with `next start`. `next dev` is not a production server. ```yaml command: "npm run start" # package.json: "start": "next start -p $PORT" ``` - `npm run build` runs automatically during the build. Check that it produces `.next/`. - Change `"start": "next dev"` if present. - Read `KEELSON_DB_URL` on the server. Putting it in a `NEXT_PUBLIC_*` variable sends it to the browser. ### Prisma Keep Prisma's `provider` set to `"sqlite"` when connecting to Managed SQLite. Change the connection method: replace the local file connection with a Prisma Client using `@prisma/adapter-libsql`, `KEELSON_DB_URL`, and `KEELSON_DB_AUTH_TOKEN` ([connection instructions](https://docs.turso.tech/sdk/ts/orm/prisma)). Use compatible versions of `@prisma/adapter-libsql` and `@prisma/client` (6.x has been tested). Configuring the adapter does not automatically apply Prisma migrations to the remote database. Ask your AI agent to verify queries with your version and how to apply generated SQL to Managed SQLite. ## Go Keelson builds with `go build -o /workspace/app .` and starts the app with `./app`. ```yaml runtime: go-slim command: "./app" ``` - Builds use `CGO_ENABLED=0`. cgo SQLite drivers such as `mattn/go-sqlite3` cannot be used. Use a pure Go libSQL client for Managed SQLite. - Handle shutdown explicitly with `signal.NotifyContext` and `server.Shutdown`, so in-flight requests finish within 10 seconds. - If you use an ORM such as GORM, check database and driver compatibility. If it cannot connect to Managed SQLite, switch to `database/sql` and a pure Go libSQL client. ## Static sites / SPAs For Vite, Next.js static exports, Astro, and similar tools, build locally and deploy the output through `assets.dir`. Keelson does not install dependencies or build static sites that have no `command`. Static sites require `type: web`. Files in `assets.dir` are included in the upload even if the folder, such as `dist/`, is in `.gitignore`. See [Configure keelson.yaml](https://keelson.dev/docs/building-apps/keelson-yaml/) for examples. For hybrid apps combining a static frontend and API server, also build the frontend locally before deploying. ## Related pages - [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/) - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [Keelson Deploy Spec](https://keelson.dev/docs/reference/deploy-spec/) — Authoritative specification for AI agents # Deploy & Operate --- # Deploy an app Deploy with an AI agent or the CLI and check that the deployment has finished URL: https://keelson.dev/docs/deploy/deploy/ Language: en --- Deploying sends your app to Keelson so people can use it. You deploy when you first set up an app and whenever you want to apply changes. Keelson is designed to work with AI agents. After the initial setup, you can ask your agent to check the configuration, deploy, and verify the result. ## What you need - Source code for a [supported app](https://keelson.dev/docs/building-apps/supported-app-types/) - A `keelson.yaml` configuration file in the app folder; ask your agent to create one if needed - The Keelson CLI installed and signed in ([Quickstart](https://keelson.dev/docs/introduction/quickstart/)) - Any API keys or other values your app needs ([Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/)) Creating an app requires the Owner, Admin, or Developer role. Updating an existing app requires its manage permission (`manage`). ## Ask your AI agent Open the app folder in your AI agent and send a request like this. The [Quickstart](https://keelson.dev/docs/introduction/quickstart/) explains how to install the Keelson Agent Skill. > Deploy this app to Keelson. Check the required configuration, then give me the app URL and the results of your checks when deployment is complete. Ask the agent to: 1. Check that the app's storage and startup settings work with Keelson. 2. Create or update `keelson.yaml` and explain where to register API keys and other required values. 3. Deploy and monitor the deployment through completion. 4. If it fails, inspect the logs and explain the cause and how to resolve it. 5. Verify the deployed app and report what was checked. The first deployment creates the app. Later deployments update the app identified by the `slug` in `keelson.yaml` within the same workspace. A slug is the name used to identify an app. ## Check the result For a web app or static site, open the reported URL and check the page and main actions. See [Verify a deployment](https://keelson.dev/docs/deploy/verify-deploy/) for a checklist and an example request for your agent. For an app that only runs scheduled jobs, check its schedule and results in the console's **Scheduled Jobs** tab instead of opening a page. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) for running a job manually. ## When changes take effect | Change | When it takes effect | |---|---| | Code and `keelson.yaml` settings | On deployment | | Secrets | On the next deployment after saving. **Apply changes** also redeploys with the new values without uploading or rebuilding the code | | App permissions, IP restrictions, and app URL | Without deployment, usually within two minutes. Changes made during an outage take effect after recovery | If the app uses its own URL internally, also see [Read the app URL in your code](https://keelson.dev/docs/deploy/public-url/#read-the-app-url-in-your-code). ## If deployment fails Give your agent any error code (`code`) and suggested action (`hint`) from the error message to help it investigate. See [Troubleshooting](https://keelson.dev/docs/deploy/troubleshooting/) for logs and help by symptom. If an update fails before traffic switches to the new version, the previous version stays accessible. Database changes are not automatically undone, however. For updates that change the database, follow [Update and restore](https://keelson.dev/docs/deploy/update-and-restore/) to check its state. ## Deploy directly with the CLI You can also deploy from a terminal without an agent. ```bash # First deployment: create and deploy the app keelson deploy --new # Register secrets with the first deployment keelson deploy --new --secrets-from-env-file .env.keelson # Subsequent deployments keelson deploy # Check configuration and archive contents without uploading keelson deploy --check --json ``` - The file passed to `--secrets-from-env-file` is excluded from the upload archive. - If you belong to multiple workspaces, add `--workspace `. - To specify the app to update, add `--app `. The uploaded source still comes from the folder where you run the command. - In scripts, use `keelson deploy --ndjson --yes` to wait for completion and receive one JSON object per line. ## What happens during deployment The following details help when inspecting configuration or logs. For web apps that serve pages or APIs from a server, deployment proceeds through these stages: 1. **Validation** — checks `keelson.yaml` and the files being uploaded. 2. **Build** — installs dependencies and packages the files needed to run the app into a container image. The build limit is 600 seconds. 3. **Startup** — starts the app with the new code and configuration. This running version is called a revision. 4. **Health checks** — checks that the app responds at `health.path` (default `/`) and the paths in `verify`. 5. **Migration** — runs `db.migrate`, if configured, to update the database structure or data. 6. **Traffic switch** — directs users to the new revision. Static sites and SPAs serve prebuilt HTML, JavaScript, and other files without starting a web server. Apps that only run scheduled jobs do not have web traffic to switch; their jobs run at the configured times. Storage and startup adjustments may include moving from a local SQLite file to Managed SQLite or reading the `PORT` supplied by Keelson. See [Supported apps and constraints](https://keelson.dev/docs/building-apps/supported-app-types/). ## Read next - [Verify a deployment](https://keelson.dev/docs/deploy/verify-deploy/) — check pages and API behavior - [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) - [App URLs and domains](https://keelson.dev/docs/deploy/public-url/) - [App status and logs](https://keelson.dev/docs/deploy/app-status-and-logs/) --- # Verify a deployment Check your deployed app and let an AI agent inspect page and API responses URL: https://keelson.dev/docs/deploy/verify-deploy/ Language: en --- After deployment finishes, open the app and check the actions you use regularly. You can ask your AI agent to check that pages and APIs respond correctly. ## What to check yourself - Can you open the app with your account? - Do the main actions, such as listing items and searching, work? - If the app saves data, can you save and read back test data? Before sharing the app, also check that the intended users have access. Being able to open it yourself does not mean others can. See [App URLs and domains](https://keelson.dev/docs/deploy/public-url/). For an app that only runs scheduled jobs, check the execution results in the console's **Scheduled Jobs** tab. ## Ask your AI agent to check > Check the main pages and APIs of the deployed app. Tell me what you verified and what remains unchecked. Use test data for checks that change data. A successful response does not prove that calculations or business rules are correct. Give the agent typical inputs and expected results so it can check those too. ## How an agent checks responses Regular app URLs require authentication, so a plain `curl` request without credentials cannot verify them. `keelson app curl` sends requests with the permissions of the account signed in to the CLI. A preview token is a temporary credential for accessing the app as that user. `app curl` obtains one internally, so you do not need to handle the token value. ### Check a single response with keelson app curl Use these commands to retrieve a page or API response: ```bash keelson app curl / # GET / keelson app curl -i /api/items # Include response headers ``` Creating, updating, or deleting data and uploading files change the actual app. These examples perform writes; adjust the paths and data for your app. ```bash keelson app curl /api/items --method POST \ --data '{"name":"example"}' \ --header 'Content-Type: application/json' keelson app curl /upload --form title=example --form photo=@sample.png ``` - The response body goes to standard output. The method, URL, and status go to standard error. With `-i`, headers also go to standard error, so you can save just the body to a file. - The default method is `GET`. Explicitly using `--method POST`, `PUT`, `PATCH`, or `DELETE` obtains a token that allows writes and lasts five minutes. - `--data` can only be used with write methods. Use `@filename` to read data from a file. - `--form` sets the method to `POST`. The CLI adds the multipart `Content-Type`; you cannot override it with `--header`. - Paths 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`. - Add `--app ` to specify the target app. Ask the agent to check `Content-Type` as well as the status. For example, a SPA might return HTML instead of a missing JavaScript file. A `200` response in that case does not mean the page will work. ### Use another tool or make multiple requests with keelson preview Issue a token directly if you want to use another HTTP client, make several requests, or choose the token lifetime. ```bash keelson preview # GET / HEAD only; default 30 minutes keelson preview --ttl 5m keelson preview --allow-writes # Also POST / PUT / PATCH / DELETE; default 5 minutes, maximum 10 keelson preview --json # Token, URL, and expiry as JSON ``` Use the `token` and `app_url` returned by `--json` in your HTTP client. Send the token as a Bearer token. ```bash curl --header 'Authorization: Bearer ' '/api/items' ``` The token is shown once on standard output and is not stored by Keelson. Do not include it in logs or reports. Only one `keelson preview` token can exist per app and user. Issuing another revokes the previous token, even if it has not expired. `keelson app curl` uses a separate internal token and does not revoke the token issued by `preview`. ## Apps deployed by someone else If you did not create the currently served deployment, the CLI provides an approval URL to open in a browser before issuing a preview token. Approval requires the app's manage permission (`manage`). You can approve your own request if you have that permission. Review the app, write access, and lifetime on the approval page, then approve token issuance. Rerun the same command with the supplied `--confirmation `. ## What these checks do and do not cover - These commands retrieve responses using your permissions. Check browser appearance and interactions, and other users' access, separately. - Static sites have no write destination, so write methods are rejected. - Even a `GET` request can change data or send email if the app implements it that way. Ask the agent to inspect the code first and avoid paths that would cause unintended changes. ## Related pages - [Deploy an app](https://keelson.dev/docs/deploy/deploy/) - [CLI commands](https://keelson.dev/docs/reference/cli/) --- # Environment variables and secrets Pass configuration values and API keys to your app URL: https://keelson.dev/docs/deploy/environment-variables/ Language: en --- You can configure API keys, service URLs, and other values for your app. Environment variables are how these values reach your code. Store confidential values, such as API keys, as Keelson secrets. | Type | Examples | Where to set it | |---|---|---| | Non-secret configuration | `NODE_ENV`, `LOG_LEVEL`, public API URLs | `env` in `keelson.yaml` | | Secret values | API keys, tokens, database connection strings, webhook signing secrets | **Secrets**, using the console, CLI, or an env file at deployment | Keep `keelson.yaml` with your source code. Do not include secret values in it; register them as secrets instead. ## Ask your agent what the app needs > List the configuration values and API keys this app needs. Document their names and purpose, and create a local file where I can enter the values. Once I have filled it in, register and apply them in Keelson. Your agent can prepare the configuration, implement code that reads the values, and register and apply secrets. Enter secret values in a local configuration file instead of pasting them into chat, then ask the agent to register them from that file. You can also enter them yourself in the console. ## Register secret values An AI agent with the Keelson Agent Skill can register secrets from a local env file, either on the first deployment or when updating an existing app. > Register the values in `.env.keelson` as this app's secrets and apply them. Do not print the values in chat or logs. The following illustrates the file format. Ask your agent to prepare the keys your app needs, then fill in their values. ```dotenv # .env.keelson OPENAI_API_KEY=sk-... SLACK_WEBHOOK_URL=https://hooks.slack.com/... ``` To enter values yourself, use the **Secrets** tab of an existing app to add, update, or delete them. After saving, only the names and update times are shown; values cannot be displayed again. You or your agent can also register and apply them with the CLI: ```bash # Register with the first deployment keelson deploy --new --secrets-from-env-file .env.keelson # Register while updating an existing app keelson deploy --secrets-from-env-file .env.keelson # Register and apply without uploading or rebuilding the code keelson secrets set --from-env-file .env.keelson --apply ``` The file passed to `--secrets-from-env-file` is excluded from the upload. Do not add files containing secrets to Git or put secret values directly in commands, where they may remain in shell history. ## Apply saved values to the app Saving a secret does not update an app that is already running. Ask your agent to both register and apply the values. They take effect during the first deployment; for existing apps, use one of these options: - If you are also changing code, run `keelson deploy`. - To apply values only, click **Apply changes** in the console's pending-changes notice. - To register and apply in one CLI command, use `secrets set --apply` as shown above. **Apply changes** and `--apply` redeploy without uploading or rebuilding the code. They use the last successful deployment's container image to start the app with the new values. Once complete, test an action that uses the API key or other changed value. ## env: non-secret values The following sections cover configuration and code details. Ask your agent to put non-secret values under `env` in `keelson.yaml`. ```yaml env: NODE_ENV: "production" LOG_LEVEL: "info" ``` - Quote all values, including numbers and booleans. Unquoted values cause deployment to be rejected. - Names starting with `KEELSON_` are rejected; Keelson supplies them. `PORT` is ignored if set, and Keelson's value is used. - Changes take effect on the next deployment. ## Declare the secrets your app needs The `secrets` section in `keelson.yaml` can list secret names, their purpose, and the conditions that make them required. Do not include values. **This declaration is optional.** Registered secrets are passed to the app as environment variables even without a declaration. Declarations document the required configuration and let deployment checks catch missing values. Ask your agent to identify the keys and requirements. ```yaml secrets: items: - name: OPENAI_API_KEY description: "OpenAI API key" - name: SLACK_WEBHOOK_URL description: "Slack webhook for notifications" required: - all_of: [OPENAI_API_KEY] message: "Set OPENAI_API_KEY" ``` - `items` lists names and descriptions, which also appear in the console. Listing a key here does not make it required. - `required` defines the conditions for deployment. If the registered secrets do not satisfy them, deployment fails with the specified reason. In this example, a missing `OPENAI_API_KEY` displays “Set OPENAI_API_KEY”. Register the value, then deploy again. - `SLACK_WEBHOOK_URL` is listed but not required, so its absence does not block deployment at this check. Without `required`, Keelson does not perform this required-secret check. Missing values may instead cause errors when the app starts or performs an action that uses the key. ## Scope and precedence | Scope | Where to set it | Use | |---|---|---| | Workspace | **Secrets** in the console | Values shared across apps, such as a common API key | | App | The app's **Secrets** tab | App-specific values or overrides of workspace values | For the same name, precedence is **app secret > workspace secret > `env`**. The app's Secrets tab shows which scope each value comes from. ## Read values in your app Server-side code reads both `env` values and secrets as environment variables. ```python import os api_key = os.environ["OPENAI_API_KEY"] ``` ```javascript const apiKey = process.env.OPENAI_API_KEY; ``` ## Values are not available during builds Neither `env` nor secrets are supplied during builds, including dependency installation and `npm run build`. The build can only read the uploaded source files. Put public values needed by a frontend build, such as `VITE_API_URL`, in configuration files within the source. These values are distributed to browsers, so do not put credentials there. ## Environment variables set by Keelson Keelson sets these platform-managed variables for apps. Do not define or override them yourself. | Variable | Purpose | |---|---| | `PORT` | The listening port for a web app. | | `TZ` | The workspace time zone. | | `KEELSON_MODE` | Indicates platform mode. | | `KEELSON_APP_ID` | The internal app ID. | | `KEELSON_WORKSPACE_ID` | The internal workspace ID. | | `KEELSON_TENANT_ID` | Compatibility alias for `KEELSON_WORKSPACE_ID`, with the same value. | | `KEELSON_DEPLOY_ID` | The internal ID of the current deployment. | | `KEELSON_APP_URL` | The standard app URL. | | `KEELSON_DIRECTORY_BASE_URL` | The Directory API URL. | Additional variables are set when features are enabled. See the [Environment variable reference](https://keelson.dev/docs/reference/environment-variables/) for the complete list, the conditions for each variable, and compatibility variables. `NODE_ENV` is not set automatically. Add it to `env` if your app needs it. ## Read next - [Environment variable reference](https://keelson.dev/docs/reference/environment-variables/) - [External integrations](https://keelson.dev/docs/building-apps/external-integrations/) - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#secrets) --- # App URLs and domains Find and change your app URL, use it in your code, and set up a custom domain URL: https://keelson.dev/docs/deploy/public-url/ Language: en --- Web apps and static sites receive a dedicated URL that you can share after deployment. You can also use a domain your company manages. The standard URL has this format. A slug is an identifier used in URLs and other settings. ```text https://--.keelson.run ``` For workspace slug `acme` and `slug: dashboard` in `keelson.yaml`, the URL is `https://acme--dashboard.keelson.run`. Keelson manages the HTTPS certificate. Find the URL on the app page in the console, in your agent's deployment report, or with `keelson status`. ## Who can open the URL Knowing the URL is not enough. A visitor must be signed in to the workspace and have permission to view the app. If IP restrictions are configured, they must also connect from an allowed network. **Share the app URL directly with App Users.** They cannot open the console, so they cannot look it up there. Copy it using one of the methods above and send it to them by chat or email. - [Authentication and login](https://keelson.dev/docs/access-control/auth-and-login/) - [Groups and app permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) - [IP restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) ## Change the URL Owners and Admins can change an app's URL after creation. Use **Public URL** in the app's console settings. The old URL stops working and does not redirect to the new one. Share the new URL with users and update bookmarks and external service settings. Before saving, select the checkbox confirming that you understand the impact. - The old URL is reserved for 30 days so another app cannot take it. Requests to it receive `410 Gone` during that period. It is released automatically after 30 days. - Use the console or `keelson apps rename ` to change the URL. The `slug` in `keelson.yaml` identifies the deployment target; changing it alone does not change the URL. Without an explicit `--app`, the next deployment looks for an app matching the new `slug` and can no longer update the original app. If you also change the app's identifier in the console, update `keelson.yaml` to match before deploying again. You can also change the workspace slug in settings, but **existing app URLs stay the same**. To use the new workspace slug in those URLs, update each app's public URL individually. ## Custom domains The Team plan lets you use a company subdomain such as `app.example.com`. You need permission to change the domain's DNS settings. If a colleague manages your domain, send them the values Keelson provides. - Available on Team: up to 10 domains per workspace and one domain per app. - Subdomains only. Root domains such as `example.com` and email domains are not supported. To send email from your domain, see [Send from your own domain](https://keelson.dev/docs/building-apps/external-integrations/#send-from-your-own-domain). - Authentication, permissions, and IP restrictions also apply through custom domains. Follow these steps: 1. An Owner or Admin registers the domain in the app settings or with `keelson domains add [--app ]`. Keelson shows the CNAME target to configure. 2. At your DNS provider, point the subdomain's CNAME to that target. In production, the target is `custom-origin.keelson.run`. Use the value shown in the console or CLI. 3. Check that the domain is active in the console or with `keelson domains verify [--app ]`. Activation may take a few minutes. Keelson automatically verifies ownership, issues the HTTPS certificate, and configures serving. ## Read the app URL in your code Use this when your app needs to create links to itself, such as links in emails. You can ask your agent to configure it. Keelson supplies the standard app URL in `KEELSON_APP_URL`. The request's `Host` header contains an internal hostname, so use `KEELSON_APP_URL` for absolute URLs in emails, redirects, or `og:url`. ```python import os base_url = os.environ["KEELSON_APP_URL"] # e.g. https://acme--dashboard.keelson.run ``` This variable is not set during local development. Use `KEELSON_MODE` to detect whether the app is running on Keelson. Changing the public URL does not immediately update the environment of a running app. Redeploy apps that use `KEELSON_APP_URL`, then check that email links and redirects use the new URL. Setting a custom domain does not change `KEELSON_APP_URL`: it still contains the standard URL. To use the custom domain in email links and similar places, ask your agent to store it as an app configuration value. See [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/). ## Related pages - [Deploy an app](https://keelson.dev/docs/deploy/deploy/) - [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) --- # App status and logs Check app status, suspend and resume apps, reserve startup slots, and inspect logs and deployment history URL: https://keelson.dev/docs/deploy/app-status-and-logs/ Language: en --- Use the console's status indicators and logs to check whether an app is available and investigate problems. Logs record app activity, errors, and access requests. ## App status Status badges appear in the app list and on each app's page. | Status | Meaning | |---|---| | **Active** | Recent requests are using one concurrent app slot | | **Sleeping** | Deployed, but not using a slot because there have been no recent requests. A stopped app starts automatically on access | | **Published** | Serving a static site or SPA. Keelson returns the page files directly | | **Suspended** | Manually stopped. **Requests do not start the app or resume serving.** Data, URL, and configuration are retained | | **Starting** / **Deploying** | Starting or stopping, or applying new code or configuration | | **Not deployed** | Created, but not yet deployed | | **Error** | The latest deployment or execution failed. The app page shows the cause | | **Deleting** | Deletion is in progress. The app disappears from the list when it finishes | | **—** | An app that only runs scheduled jobs. Check individual results in the **Scheduled Jobs** tab | Active and Sleeping describe usage, not whether a server process is currently alive. Web apps stop after a period without requests and start on the next request. See [When apps run](https://keelson.dev/docs/building-apps/how-apps-run/). Apps accessed within the last five minutes normally occupy a slot. Priority startup reserves a slot even without requests. When no slot is available, users see a page asking them to wait. An unreserved slot is released about five minutes after the last request to its app. An Error badge may refer to the latest failed deployment even while the previous version remains available. Check the failure in **Overview** and try opening the app. ### Suspend and resume To temporarily stop an app, open **Settings → Suspend** on its page. This applies to web apps and static sites or SPAs. While suspended, requests, webhooks, and scheduled jobs do not start it, and static files are not served. The standard Keelson URL shows a stopped page, and custom-domain serving also stops. Data, URLs, and configuration are retained. Use **Resume** in the app page header to resume it. Deploying a static site or SPA while it is suspended does not resume serving; you must explicitly resume it. Static serving changes propagate gradually, so stopping or resuming may take a little time. Apps that only run scheduled jobs have no console Suspend action. To stop their schedules, set each job's `enabled` to `false` and deploy. Set it back to `true` and deploy to resume. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/). ### Priority startup Turn on **Priority start** in the app page header to reserve one concurrent app slot. This prevents the app from being blocked because all slots are occupied. - The app still stops after a period without requests. Users may need to wait for startup on the next request. - On paid subscriptions, available on Plus and higher plans; you can reserve up to one fewer than your total slot count. During the trial, all plans can enable it for up to 2 apps. ## Logs Choose the log that matches what you are investigating. | What you want to know | Where to look | What it records | |---|---|---| | Why a deployment failed | **Details** for a deployment in **Deploys** | The failure summary and messages for you, such as warnings about your `keelson.yaml`. **Details** appears only on deployments that failed or have warnings | | Why an action failed in the app | App logs under **Logs** | Activity and errors written by the app to standard output and standard error | | Why a user was denied access | Access logs under **Logs** | Who accessed which path, when, and whether access was allowed or denied | | Whether a scheduled job succeeded | Run history under **Scheduled Jobs** | Start time, result, duration, and logs for each run | Give your agent the app name, time, and what happened: > In this app, [action] produced an error at [date and time]. Check the status and logs, then explain the cause and how to resolve it. ### Log ranges and the CLI App logs can be filtered between web and scheduled-job (cron) execution. The console shows the most recent 200 lines; exports include up to 5,000 lines. Access logs can be filtered by users, machines (app tokens), and webhooks. Retention depends on your [plan](https://keelson.dev/docs/workspace/plans-and-limits/). Use `keelson logs` from the CLI. For app logs in a format an agent can process, use `keelson logs app --json`. See [Identify the failing stage](https://keelson.dev/docs/deploy/troubleshooting/#identify-the-failing-stage) for logs to inspect after a failed deployment. ## Deployment history and rollback The **Deploys** tab lists past deployments. You can download each deployment's source. Deployments that failed or have warnings also show **Details**. A rollback returns the code to an earlier deployment. It does not restore database contents, and static sites or SPAs require redeploying earlier files. See [Restore an earlier version of the code](https://keelson.dev/docs/deploy/update-and-restore/#restore-an-earlier-version-of-the-code) for instructions and limits. **Redeploy** in the app page header restarts the app using the last successful deployment's runnable files. Applying secrets also redeploys without uploading or rebuilding the code. See [Apply saved values to the app](https://keelson.dev/docs/deploy/environment-variables/#apply-saved-values-to-the-app). ## Delete an app To retire an app, use **Settings → Danger Zone** to delete it. Browser approval is required, and you cannot undo the deletion yourself. See [Retire an app](https://keelson.dev/docs/deploy/update-and-restore/#retire-an-app) for what to export first and how long data is retained. Use Suspend above if you only want a temporary stop. ## Related pages - [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) - [Troubleshooting](https://keelson.dev/docs/deploy/troubleshooting/) - [CLI commands](https://keelson.dev/docs/reference/cli/) --- # Update and restore Apply app changes safely and understand what you can recover when something goes wrong URL: https://keelson.dev/docs/deploy/update-and-restore/ Language: en --- You can deploy app changes or return to earlier code or data after a problem. Choose the action that matches what you need. | Goal | Action | |---|---| | Apply changes | [Deploy again](#apply-app-changes) | | Return to the code before an update | [Roll back](#restore-an-earlier-version-of-the-code) | | Recover accidentally changed or deleted data | [Use a database restore point](#recover-accidentally-deleted-data) | | Temporarily stop access | [Suspend](#retire-an-app) | | Retire the app | [Delete](#retire-an-app) | These actions require the app's manage permission (`manage`). Database restores and app deletion also require approval in a browser. **Restoring code does not restore the database or files to their earlier state.** See [What each operation restores](#what-each-operation-restores) for the full comparison. ## Apply app changes Tell your agent what changed and which actions to check. > Deploy these changes to Keelson. Check the configuration and upload contents before deploying, then verify that [action] works afterward. 1. Check your changes in your local development environment. 2. Ask the agent to deploy. With the CLI, use `keelson deploy --check --json` to inspect configuration and upload contents, then `keelson deploy` to apply them. 3. After deployment, open the app and test the changed behavior. For apps that only run scheduled jobs, check the run history. See [Verify a deployment](https://keelson.dev/docs/deploy/verify-deploy/). If deployment fails before the traffic switch, the previous version remains accessible. However, changes made by a partially completed `db.migrate` remain. See “Change the database schema” below. See [When changes take effect](https://keelson.dev/docs/deploy/deploy/#when-changes-take-effect) for details such as the deployment or Apply step required after saving secrets. ## Restore an earlier version of the code A rollback returns the code to an earlier deployment. The target must be a successful deployment whose container image, the packaged files used to run the app, is still available. You can ask your agent: “After [problem], check whether we can return to the previous code, including any impact on data.” The CLI commands are: ```bash keelson rollback # Return to the previous successful deployment keelson rollback # Return to a specified deployment ``` - The app starts 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 with an older deployment method cannot be rollback targets.** - Secrets use their current values. - If the target deployment's `keelson.yaml` includes `db.migrate`, it also runs during rollback before traffic switches. A rollback can therefore modify the database. - **The database is not restored.** If the update changed its schema, check that the older code can use that schema. Even adding a column can affect compatibility through constraints or defaults. Ask your agent to check compatibility and consider a database restore if needed. - Rollback cannot run while a deployment is in progress. Past deployments appear in the console's **Deploys** tab, where you can download their source. For a **static site or SPA**, redeploy the earlier files. Either return to an earlier Git commit and build locally, or download the source of an earlier deployment from the Deployments tab: the contents of `assets.dir` and `keelson.yaml`. To use the downloaded archive, extract it and run `keelson deploy` in the folder containing `keelson.yaml`. No rebuild is needed. ## Recover accidentally deleted data Restore Managed SQLite data (`db.mode: libsql`) from **Restore points** on the app page in the console. 1. Select a point on the restore timeline: a daily backup, a manually saved snapshot, or a specific time using point-in-time recovery (PITR). See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for retention counts and recovery windows. 2. You can **Download** the selected restore point to inspect it. This downloads that historical state, not the current database. 3. Select **Restore** and approve the operation in your browser. 4. Open the app and check the data after restoration. What happens during a restore: - **Database writes are temporarily unavailable during the switch.** Notify users and choose a quiet period. - **All writes after the selected point are lost.** This does not recover only the deleted records. To keep the current state too, create a manual snapshot of the current database and download it before restoring. Ask your agent how to bring back any later changes you still need. - The state before restoration is retained for 72 hours, during which you can undo the restore. - A restore affects only one app's database, not other apps. - Files / Media files and external databases are not included. Before a major change, take a manual snapshot to create a known recovery point. You can create five per app per day. ## Change the database schema A migration updates the database structure, for example by adding fields. Ask your agent to check that returning to the earlier code will still work: > Before changing the database schema, check compatibility with the previous code and whether the migration can safely run again after a failure. For changes that may require a restore, explain the impact and recovery steps before running them. ### Implementing migrations The command in `db.migrate` runs before traffic switches during a deployment or rollback. - If it fails, the previous version remains accessible, but **partially applied database changes are not automatically undone**. Inspect `keelson logs deploy ` and the database state before making corrections. - Make repeated runs safe so the same change is not applied twice. This property is called idempotency. Track applied changes or use constructs such as `IF NOT EXISTS`, and check what happens after a partial failure. - Check compatibility with the previous code even when adding columns or tables. For higher-impact changes, such as removing columns or changing types, consider taking a snapshot immediately beforehand and running during a quiet period. ## Retire an app Suspending and deleting have different effects. | Action | What happens | Can you undo it? | |---|---|---| | **Suspend**, under **Settings → Suspend** for web apps and static sites / SPAs | Requests, webhooks, and cron do not start the app. The URL shows a stopped page. Data, URL, and configuration remain | Yes, use **Resume** | | **Delete**, under **Settings → Danger Zone** | Removes the app, URL, deployment history, and secrets. Requires browser approval | **No** | Apps that only run scheduled jobs have no console Suspend action. Set each job's `enabled` to `false` and deploy to stop scheduling; set it back to `true` and deploy to resume. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/). Export these before deletion: - **Database data** — for the current state, create a manual snapshot and download that restore point. - **Files files** — add download functionality to the app and export them. - **Media files** — create viewing links from the file IDs recorded by the app and save the files before deleting. Neither Files nor Media has a console or CLI listing or download feature. See [File management and capacity](https://keelson.dev/docs/building-apps/files-and-media/#file-management-and-capacity). - **Source code** — keep it in Git or download it from a deployment in the Deployments tab. - **Secret values** — saved values cannot be displayed again. Retrieve them from where you originally stored them or from the issuing service. After deletion, database backups, Files files, and non-sensitive configuration notes (app name, URL, cron definitions, and environment variable and secret **names**) are retained in isolation for seven days. This allows discretionary assistance with accidental deletion; it does not guarantee recovery. See [Billing and subscriptions](https://keelson.dev/docs/workspace/billing/#apps-and-data-after-your-subscription-ends) for what happens when a subscription ends. ## What each operation restores “Rollback” below means returning the code to an earlier deployment. “Database restore” means returning Managed SQLite to a restore point. | Item | Restored by rollback? | Restored by database restore? | Notes | |---|---|---|---| | Code (image) | **Yes** | No | Starts the target image without rebuilding. Excludes static sites / SPAs | | `keelson.yaml` settings (`env`, `crons`, `assets`, `health`) | **Yes** | No | Uses the target deployment's configuration | | Secret values | No | No | Always uses **current** values. Change them manually in the console | | Managed SQLite data | No | **Yes** | Daily backups, manual snapshots, and PITR | | Schema changes made by `db.migrate` | No | **Yes** | Returns the database structure to the restore point | | [Files SDK](https://keelson.dev/docs/building-apps/sdk/) files | No | No | Not backed up; retention is the app's responsibility | | [Media SDK](https://keelson.dev/docs/building-apps/sdk/) files | No | No | Cannot be overwritten; save a new file to replace one | | External databases, such as PostgreSQL | No | No | Use the external service's backups | | External actions, such as Slack posts, emails, or payments | No | No | Rollback does not undo them. Avoid duplicates when retrying | | App permissions, IP restrictions, and public URL | No | No | Managed independently of deployment. Revert manually | ## Related pages - [App status and logs](https://keelson.dev/docs/deploy/app-status-and-logs/) — suspension, deployment history, and logs - [Managed SQLite backup and restore](https://keelson.dev/docs/building-apps/persistent-storage/#backups-and-recovery) - [Troubleshooting](https://keelson.dev/docs/deploy/troubleshooting/) --- # Troubleshooting Find the right logs and next steps when a deployment or app fails URL: https://keelson.dev/docs/deploy/troubleshooting/ Language: en --- Use this page when deployment fails or your app does not behave as expected. Tell your AI agent what happened and share the error message to ask it to investigate. ## Find your symptom | Symptom | What to check | |---|---| | Cannot deploy | [Identify the failing stage](#identify-the-failing-stage) | | Cannot open the app | [Permissions](#a-user-cannot-access-the-app), [available slots](#no-running-slot-available-503), [response time limits](#504-gateway-timeout) | | Changes do not appear | [Page updates](#the-page-looks-the-same-after-an-update), [applying secrets](#updated-secrets-have-not-taken-effect) | | Data is not retained | [Storage location](#data-disappears) | | Scheduled or background work does not run | [Scheduled jobs](#scheduled-jobs-cron-do-not-run), [background processing](#background-work-does-not-run) | ## Ask your AI agent to investigate An AI agent with the Keelson Agent Skill can use the CLI to retrieve app status, deployment logs, app logs, and access logs. It uses the permissions of the account signed in to the CLI, so that account needs permission to view the app's logs. You can give the agent the app name, time, and symptom without finding and pasting the logs yourself. > In this app, [action] causes [symptom]. It happened at [date and time]. Check the status and logs, then explain the cause and how to resolve it. Include any error code (`code`), message (`message`), and suggested action (`hint`). If the issue cannot be resolved by changing the app, such as a service outage, gather the information under [Contact support](#contact-support). ## Identify the failing stage On the app page in the console, **Overview** summarizes the failure, **Details** on a deployment under **Deploys** shows its failure details, and **Logs** shows app logs. Start with the overview, then ask your agent to investigate the details. ### CLI commands and log details A `deploy_id` identifies a deployment. Find it in CLI output or the console's **Deploys** tab. ```bash keelson status # App and latest deployment status keelson diagnose # Diagnose a failed deployment keelson logs deploy # Progress and stored failure details keelson logs app # App standard output and standard error ``` Even if the app exited during startup, `keelson logs deploy` and `keelson diagnose` can show saved startup logs. These may identify the failing code or missing configuration.
Which logs are saved Failed deployments retain build and startup details. Use them to investigate dependency, Dockerfile, secret, configuration, and migration errors afterward. Check saved startup logs for tracebacks and process errors. Build output for successful deployments is not retained. If neither progress logs nor saved failure details are available, the CLI explains why logs are unavailable.
| Stage | Where to look | Common causes | |---|---|---| | Before upload: CLI preflight checks | Command output | Missing `go.mod` for Go; only pnpm / yarn lockfiles for Node; `pip install` in `command`; missing `db.mode`; unquoted `env` values | | Validation | `code` and `message` | Leftover `workers` / `databases`, reserved names, plan limits | | Build | Deployment logs | Dependency resolution failure, mismatched `package.json` and `package-lock.json`, private registries, Go packages requiring cgo, build script errors | | Startup | Saved app startup logs from `keelson logs deploy ` | Not listening on `0.0.0.0`, hardcoded `PORT`, exceptions caused by missing secrets | | Health checks | Saved startup logs, then app logs | App exits during startup, `/` returns 5xx, startup exceeds the health-check deadline | | Migration | Deployment logs | SQL errors in `db.migrate`. The previous version remains accessible, but **database changes made by the migration are not automatically undone**. Check the database before correcting the issue | | Running but behaving unexpectedly | App logs and access logs | See the common problems below | ## Common problems ### I pressed Ctrl-C during deployment Ctrl-C stops only this client's monitoring. Deployment continues on the server and may still switch traffic to the new version. Check progress using the `deploy_id` shown by the CLI. ```bash keelson status ``` If you do not want the new version, restore the previous code after deployment completes. This command returns to a previous successful deployment whose container image is still available. Database changes are not restored. ```bash keelson rollback --app ``` Static sites and SPAs do not support rollback; redeploy the earlier files. See [Restore an earlier version of the code](https://keelson.dev/docs/deploy/update-and-restore/#restore-an-earlier-version-of-the-code). If you need to stop access temporarily, for example because no rollback target is available, suspend the app after deployment finishes. ```bash keelson app stop --app ``` ### It works locally but crashes on Keelson Configuration or storage may differ between your computer and Keelson. Ask your agent to check: - **Listening only for local connections** — change the listening address from `localhost` to `0.0.0.0`. - **A hardcoded port** — read the `PORT` environment variable. - **Files available only locally** — examples include `.env`, local database files, or files excluded by `.gitignore`. The `archive.excluded` field from `keelson deploy --check --json` shows what was left out. - **Missing environment variables** — values present only in your local `.env` are not available on Keelson. Declare the names in `secrets` and register the values. - **Local SQLite files** — files in `/tmp` or the app directory do not survive restarts (`/data` does not exist and cannot be created). Consider migrating to Managed SQLite. ### A Node app's lockfile is rejected Keelson's Node builder uses npm, so pin dependencies with `package-lock.json`. Ask your agent to run `npm install` in the project root, check dependency consistency, and deploy again. Keep the generated lockfile with your source code. You do not need to remove `pnpm-lock.yaml` or `yarn.lock`. They can coexist with `package-lock.json`; the build uses `package-lock.json`. ### Data disappears Check whether the app is storing data in files on its server. Files in `/tmp` and the app directory are lost on restart, redeployment, or idle shutdown. Use a [database](https://keelson.dev/docs/building-apps/persistent-storage/) or the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/). ### Background work does not run Server-side work that continues after sending a response is not guaranteed to finish. In-process timers also stop when the app is stopped. Ask your agent to finish work required by the current request before returning the response, enqueue work that continues after the response as a [background task](https://keelson.dev/docs/building-apps/background-tasks/), and move recurring work to Keelson scheduled jobs. Another option is to store pending work in the database and process it with a scheduled job, but it must wait until the next run. Browser-side timers are not subject to this restriction. See [When apps run](https://keelson.dev/docs/building-apps/how-apps-run/). ### Scheduled jobs (cron) do not run - Is the schedule more frequent than the plan's minimum interval? Deployment rejects it. - Is the previous run still in progress? Overlapping runs are skipped. - Has the monthly execution limit been reached? Further runs are skipped for that month. - Is the app suspended? - Check the run history in **Scheduled Jobs** for the reason a run was skipped. ### The page looks the same after an update Your browser may have cached old files. Confirm that deployment completed, then force a refresh: `Cmd + Shift + R` on Mac or `Ctrl + Shift + R` on Windows. Static site updates propagate gradually and may take a little time after deployment completes. If nothing changes, ask your agent to check the target app and the files that were uploaded. ### A user cannot access the app 1. Are they a workspace member, and have they accepted their invitation? 2. Is their membership active, rather than deactivated? 3. Do they belong to a group with view permission for the app? 4. Are they signed in with the registered account? 5. Are IP restrictions denying access? The 403 page shows the source IP. See [Troubleshooting access](https://keelson.dev/docs/access-control/members-and-permissions/#troubleshooting-access). ### No running slot available (503) The concurrent app limit has been reached. Apps accessed within the last five minutes and apps with Priority startup occupy slots. Wait about five minutes after requests stop to an app using an unreserved slot, enable [Priority startup](https://keelson.dev/docs/deploy/app-status-and-logs/#priority-startup) for an important app, or upgrade your plan. ### 504 Gateway Timeout This can occur when a response takes too long. Keelson allows 120 seconds for the response to begin, 300 seconds in total including streaming, and 120 seconds between streamed responses. See [HTTP time limits](https://keelson.dev/docs/workspace/plans-and-limits/#http-time-limits). Ask your agent to find the slow operation and consider shortening it, streaming results incrementally, or moving work to scheduled jobs. Streaming does not remove the time limits. Even after a 504, the server may have completed a save or send operation. Check the result before trying again. Your implementation needs idempotency: repeated requests must not create duplicate records or messages. ### A webhook returns 401 or 403 - Does the path start with `/api/webhooks/` or `/api/external/`? - For `/api/external/`: is the path declared in `auth.endpoints`, and does the method match the declaration? (`/api/webhooks/` needs no declaration; `methods` is not applied there) - Do the token scope and allowed IPs match? See [External integrations](https://keelson.dev/docs/building-apps/external-integrations/). ### Updated secrets have not taken effect Saving secrets does not update an app that is already running. Ask your agent to apply the values too. If doing it yourself, deploy when you also have code changes, or click **Apply changes** in the console for value-only changes. Apply redeploys without uploading or rebuilding the code. See [Apply saved values to the app](https://keelson.dev/docs/deploy/environment-variables/#apply-saved-values-to-the-app). ## Contact support For a platform error such as `deploy.platform.error`, or if these steps do not resolve the issue, use the [contact form](https://keelson.dev/contact/). Include the following to help us investigate: | Information | Where to find it | |---|---| | Workspace slug | `slug` in `keelson workspaces list --json`. URLs may still use a name from before a rename | | App slug | `slug` in `keelson.yaml` or `keelson apps list --workspace --json` | | `deploy_id` | Output of `keelson status --json` or the console's **Deploys** tab | | Error `code`, `message`, and `hint` | CLI output. Add `--json` to retrieve them together | | Date, time, and time zone | Used to find the relevant logs | | For access problems, the user's email and the IP on the 403 page | See [A user cannot access the app](#a-user-cannot-access-the-app) | Do not send app source code or secret values. If needed, support will ask for specific information. Use the same contact form for questions about pricing, subscriptions, or invoices. See [Billing and subscriptions](https://keelson.dev/docs/workspace/billing/). ## Related pages - [Error codes and remedies](https://keelson.dev/docs/reference/error-codes/) - [App status and logs](https://keelson.dev/docs/deploy/app-status-and-logs/) - [CLI commands](https://keelson.dev/docs/reference/cli/) # Users & Access --- # Authentication and login Control who can use your apps and understand how users sign in. URL: https://keelson.dev/docs/access-control/auth-and-login/ Language: en --- Keelson lets you restrict internal apps to workspace members who have permission to view them. Users open the app URL and sign in on Keelson's login page. Knowing the URL alone does not give someone access. Keelson provides the login page and manages login sessions, so you do not need to build these into your app. To choose who can use an app, see [Share an App with Your Team](https://keelson.dev/docs/access-control/share-with-team/). ## Login methods App users can sign in with any of the following: - **Google account** - **Microsoft account** - **Email address** (a one-time login code sent by email) Users must sign in with the email address registered in the workspace. If they use several accounts, they need to select the registered one. Creating a workspace (signing up for the console) requires a Google or Microsoft account. ## What happens when you sign in 1. Open the app URL. 2. If you are not signed in, Keelson displays its login page. 3. Sign in with Google, Microsoft, or the code sent to your email. 4. You return to the original app. It opens if you have view permission. While your login session remains active, you can open other apps in the same workspace without signing in again. You still need permission to view each app. If an app does not open, see [Troubleshooting access](https://keelson.dev/docs/access-control/members-and-permissions/#troubleshooting-access). ## Use the signed-in user's information in your app Your app can show the user's name or display only their own data. You can ask your AI agent: "Use the identity information provided by Keelson to show users only their own data." Keelson passes identity information to your server-side code. Your app must implement the rules that determine which data a user can view or update. See [Use the Signed-in User's Identity](https://keelson.dev/docs/building-apps/using-identity/).
How identity information is passed (HTTP headers) Keelson adds identity information to authenticated requests as HTTP headers. The main headers are: | Header | Contents | |---|---| | `X-Keelson-User-Id` | User ID | | `X-Keelson-User-Email` | Email address | | `X-Keelson-User-Name` | Name | | `X-Keelson-User-App-Perms` | Permissions for this app | Headers with these names supplied by the user are removed and replaced with identity information verified by Keelson. Your app uses this information to identify the user. The browser's `Authorization` and `Cookie` headers are also removed.
## Order of access checks When someone opens an app in a browser, Keelson checks their login session and permissions. The component that performs these checks before requests reach the app is called a proxy. ```text Browser │ ▼ Keelson 1. Is the user signed in? 2. Are they an active workspace member? 3. Do they have permission to view this app? 4. Does their network meet the IP restrictions? │ ▼ App Receives requests that pass every check ``` - If check 1 fails, the user is redirected to the login page. - If check 2 or 3 fails, an access denied page appears. - If check 4 fails, an access denied page (HTTP 403) shows the IP address the user is connecting from. IP restrictions limit access by the network a user connects from. **They do not apply to static sites, SPAs, or the static files of hybrid apps.** Login, membership, and view permission checks still apply. See [IP Restrictions](https://keelson.dev/docs/workspace/ip-allowlist/). ## Requests without a browser login Notifications from external services (webhooks) and API calls use an app token instead of a browser login. If the sender signs requests, your app must also verify the signature. A signature alone does not grant access. Configure the paths and methods allowed for API calls in `auth.endpoints` in `keelson.yaml`. This setting does not apply to webhooks, so check allowed paths and methods in your webhook handler. Tell your AI agent which service you want to connect and which notifications or data you want to receive, and ask it to configure and implement the integration. See [External Integrations](https://keelson.dev/docs/building-apps/external-integrations/). ## Related pages - [Members and Roles](https://keelson.dev/docs/access-control/members-and-permissions/) - [Groups and App Permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) - [IP Restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) --- # Members and roles Choose roles for people who use, build, or manage apps, and manage workspace membership. URL: https://keelson.dev/docs/access-control/members-and-permissions/ Language: en --- Add the people who will use your apps as workspace members. Choose a role based on whether each person uses apps, builds and updates them, or manages members and workspace settings. Users must be members to access apps. The apps they can use depend on the [app permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) assigned to their groups. ## Roles Choose App User for people who only use apps, and Developer for people who build and update them. Choose Admin for people who manage members and workspace settings, or Owner for people who also handle billing and manage other Owners. | Role | Main responsibilities and capabilities | |---|---| | **Owner** | Responsible for the workspace. Manages members, including other Owners, settings, and billing | | **Admin** | Manages members other than Owners, and workspace settings | | **Developer** | Builds, deploys, and updates apps. Cannot manage members or change workspace settings | | **App User** | Uses apps they have permission to view. Does not perform management operations in the console | Owners, Admins, and Developers count toward the plan's **Developers seats**, the allowance for people who build and manage apps. App Users do not count toward this allowance, and their number is unlimited. See [Plans and Limits](https://keelson.dev/docs/workspace/plans-and-limits/). Owners and Admins can change roles on the **Members** page. Only an Owner can assign or change the Owner role. Every workspace must have at least one Owner. ## How members join | Method | How membership starts | |---|---| | [Invitation](https://keelson.dev/docs/workspace/invite-members/) | An Owner or Admin invites an email address. The recipient opens the link and accepts the invitation | | Join URL | Someone with an allowed email domain opens the URL and signs in to join. No administrator approval is needed | | Find a workspace and request to join (Discover) | Someone with an allowed email domain submits a request, which an Owner or Admin approves | If someone has a valid invitation, signing in with the invited email address and opening an app may also accept the invitation automatically. An Owner or Admin can check the **Members** page to confirm they have joined. An email domain is the part after `@` in an email address. Allowing your company's domain lets people with those addresses use the join URL or Discover. See [Domains and Join Policies](https://keelson.dev/docs/workspace/domain-join/) for setup (Team plan and above). Members who join through domain-based joining receive the App User role. ## Disable a member To stop someone's access to all apps in the workspace, for example when they leave the company, an Owner or Admin can select **Deactivate** on the **Members** page. To change only which apps they can use, for example after a department transfer, update their [groups or app permissions](https://keelson.dev/docs/workspace/groups-and-app-access/). - Access checks may take time to reflect the change. Saving it does not necessarily stop all access immediately. - Disabling does not delete the account. Select **Reactivate** to restore access. - You cannot disable yourself or the last Owner. Admins cannot disable Owners. ## How roles relate to app permissions Members automatically belong to the following groups based on their role. These are called system groups, and their membership cannot be edited manually. | Role | Automatic group membership | |---|---| | **Owner** | `owners` `developers` `everyone` | | **Admin** | `admins` `developers` `everyone` | | **Developer** | `developers` `everyone` | | **App User** | `everyone` | New apps grant view permission to `everyone` (all members) and manage permission to `developers` (Owners, Admins, and Developers). Manage permission includes view permission. These group assignments determine who can use and manage an app. Changing a role also changes automatic group membership, which can affect app access. To limit an app to a specific team, see [Share an App with Your Team](https://keelson.dev/docs/access-control/share-with-team/). ## Troubleshooting access First, check that you are signed in with the email address that was invited or registered. Ask an Owner or Admin to check your membership status, and someone who manages the app to check your app permissions. 1. **Are you signed in with the registered account?** If you use several Google or Microsoft accounts, check the email address. 2. **Have you joined the workspace?** Check the **Members** page. If membership is not complete, ask the person to open and accept a valid invitation link. 3. **Has the member been disabled?** Check **Deactivated** on the **Members** page. 4. **Does the member have view or manage permission?** Open the app's **Permissions** tab and check the assignments for their groups. 5. **Are IP restrictions blocking access?** The denial page shows the IP address the user is connecting from. See [IP Restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) for where these restrictions apply. ## Related pages - [Groups and App Permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) - [Invite Members](https://keelson.dev/docs/workspace/invite-members/) - [Domains and Join Policies](https://keelson.dev/docs/workspace/domain-join/) - [Authentication and Login](https://keelson.dev/docs/access-control/auth-and-login/) --- # Share an app with your team Give your sales team and app managers access, share the URL, and verify who can open the app. URL: https://keelson.dev/docs/access-control/share-with-team/ Language: en --- You can limit an app to a specific team and share its URL. This guide gives access to the sales team and the people who manage the app. Inviting members and creating groups requires the Owner or Admin role. Changing app permissions requires manage permission for that app. These features are available on all plans. These settings determine who can open the app. Business rules such as "users can see only their own requests" or "only a supervisor can approve requests" must be implemented inside the app. ## 1. Invite members Open **Members** and choose the invite action. Enter the sales team's email addresses and select **App User**. This role is for people who only use apps and does not consume Developers seats, the allowance for people who build and manage apps. Each recipient opens the email link, signs in with the invited email address, and accepts the invitation. See [Invite Members](https://keelson.dev/docs/workspace/invite-members/) for details. You do not need to invite someone who has already joined. If a recipient has a valid invitation, signing in with the invited address and opening an app may also accept it automatically. Before adding someone to a group, check the **Members** page to confirm they have joined the workspace. For larger organizations, [domain-based joining](https://keelson.dev/docs/workspace/domain-join/) lets employees join without individual invitations on Team plans and above. ## 2. Create a group A group is a set of members you can assign app permissions to together. Open **Members → Groups**, create a group named **Sales**, and add the sales team members. Each group has a display name and a key used in settings and commands. This example uses `sales` as the key. You cannot change the key after creation, but you can change the display name and membership. ## 3. Choose which groups can use the app Open the app's **Permissions** tab. View permission lets someone open and use the app. Manage permission lets someone deploy the app and change its settings. **Manage permission also includes viewing the app.** New apps grant view permission to `everyone` (all members) and manage permission to `developers` (Owners, Admins, and Developers). Adding Sales to the view groups alone leaves `everyone` in place, so everyone can still open the app. 1. Select **Edit** under **View Permission**. 2. **Clear Everyone (`everyone`)** and **select Sales (`sales`)**. 3. Select **Save** under **View Permission**. In this example, leave manage permission assigned to `developers`. View and manage permissions are saved separately, so save changes to view permission in the view section. The sales team and members of `developers` can now open the app. App Users outside Sales cannot. If this app's permissions were changed before, check for any other view or manage groups that remain assigned. To change who can manage the app as well, see [Groups and App Permissions](https://keelson.dev/docs/workspace/groups-and-app-access/). Anyone with manage permission can open the app even if they do not belong to a view group. Access checks may take time to reflect saved changes. Once applied, the updated permissions also apply to users who are already signed in. ## 4. Share the URL Copy the URL from the app page (for example, `https://acme--sales-tool.keelson.run`) and share it in chat or email. App Users cannot look up app URLs in the console, and invitation emails do not include app URLs. Include a note such as "Sign in with the email address you were invited with" to help people who use several accounts. ## 5. Test with people who should and should not have access Check that intended users can open the app and that someone with neither view nor manage permission cannot. | Check | Expected result | |---|---| | A Sales group member opens the URL | The app appears after login | | An app manager outside the Sales group opens the URL | The app appears after login | | An App User outside Sales, with no view or manage permission through another group, opens the URL | An access denied page appears after login | | Someone who is not signed in opens the URL | Keelson's login page appears; the app is not shown | If no one outside the intended audience is available to test, invite another email address of your own as an App User and test without adding it to Sales. You can check the saved settings in the app list's **Access** column. When view access is restricted, it shows a member count; hover over the count to see the group names. Use the checks above to verify actual access. ## Troubleshooting | Problem | What to check | |---|---| | A sales team member cannot open the app | Confirm they have joined the workspace, belong to `sales`, and are signed in with their registered email address | | Someone outside Sales can open the app | Check whether `everyone` or another view group is still assigned, or whether the person belongs to a manage group | | Saved settings do not change access | Changes may take time. Wait briefly, reload, and check the saved **Permissions** settings and the account used for testing | For other checks, including disabled members and IP restrictions, see [Troubleshooting access](https://keelson.dev/docs/access-control/members-and-permissions/#troubleshooting-access). ## Ask an AI agent to configure access An AI agent that can use the CLI can create groups and configure app permissions. Give it the workspace, app name, email addresses of the people to include, and who should retain manage access. The same permissions are required as when using the console. > Configure `sales-tool` for the sales team and app managers. Add alex@example.com and sam@example.com, who have already joined the workspace, to the Sales group (key: `sales`). Set view access to `sales` and manage access to `developers`. Afterward, tell me which groups have view and manage access. After the agent finishes, test access with the accounts described in step 5.
CLI example Create a group and add members who have already joined the workspace. These commands make the same changes as steps 2 and 3 in the console. You do not need to run them if you have already configured access in the console. ```bash keelson groups create sales --name "Sales" keelson groups members add sales alex@example.com sam@example.com keelson access set --app sales-tool --view sales --manage developers ``` Pass the view and manage groups you want to keep to `access set`. This example sets view access to `sales` and manage access to `developers`.
## Show users only their own data The settings above determine who can open the app. To show each person only their own requests, you can ask your AI agent: > Use the signed-in user's identity provided by Keelson to show only their own requests. Prevent them from retrieving or updating someone else's request, even if they supply its URL or ID, and verify that this protection works. Alongside filtering the interface, your server-side code must check permissions when retrieving or updating data. Ask the agent to report how it verified that users cannot retrieve or change another person's data. Use the user ID supplied by Keelson (`X-Keelson-User-Id`) or group information for these checks. Do not identify a user solely from values sent by the browser. See [Use the Signed-in User's Identity](https://keelson.dev/docs/building-apps/using-identity/) for implementation details.
Workspace, app, and business permissions | Permission | Who configures it and where | Examples | |---|---|---| | Workspace management (roles) | Assigned by an Owner or Admin. Only Owners can assign or change the Owner role | Inviting members or handling billing | | App view and manage permissions (`view` / `manage`) | Assigned to groups by someone with manage permission for that app | Opening the app, deploying it, or changing settings | | Business permissions inside the app | Implemented by the app | Viewing only your own requests or approving requests as a supervisor | App manage permission is for people who deploy the app and change its settings. They are not necessarily the people who approve business requests. See [Members and Roles](https://keelson.dev/docs/access-control/members-and-permissions/) for role details.
## Related pages - [Groups and App Permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) — permissions and CLI commands - [Invite Members](https://keelson.dev/docs/workspace/invite-members/) - [IP Restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) — also restrict access by network - [App URLs and Domains](https://keelson.dev/docs/deploy/public-url/) # Workspace & Billing --- # Invite members Invite workspace members by email and manage invitations. URL: https://keelson.dev/docs/workspace/invite-members/ Language: en --- To let colleagues use your apps, invite their email addresses to the workspace. Choose a role based on whether they will only use apps or also build and update them. **Owners and Admins** can invite members on all plans. The process is complete when the recipient accepts and you can confirm their membership on the **Members** page. ## Choose a role The allowance for people who build and manage apps is called **Developers seats**. App Users do not use these seats. | Person you are inviting | Role | Developers seat | |---|---|---| | Someone who uses apps | **App User** | Not used | | Someone who builds and deploys apps | **Developer** | Used | | Someone who also manages members | **Admin** | Used | | Someone who shares responsibility for the workspace | **Owner** | Used | Admins cannot invite someone as an Owner. Developers and App Users cannot invite other users. An invitation for a Developer, Admin, or Owner reserves one Developers seat when sent, even before the recipient accepts it. You cannot invite someone to these roles when the seat limit has been reached. You can invite any number of App Users regardless of available seats. ## Send invitations (Owner / Admin) 1. Open **Members** in the console and select **Invite Member**. 2. Enter email addresses (up to 50 at a time). 3. Choose a role. 4. Select **Send Invitations**. Each recipient automatically receives an email with an invitation link. The results screen shown immediately afterward also lets you copy links to share directly. If you close that screen and later need a link, use **Resend invitation** on the **Members** page to issue and share a new one. Invitations expire **3 days (72 hours)** after creation. ## Accept an invitation 1. Open the link in the email. 2. Sign in with a Google or Microsoft account using **the invited email address**, or use a one-time code sent by email. Signing in automatically accepts the invitation. If you are already signed in with the invited address, opening the email link joins the workspace immediately. If you are signed in with a different email address, sign in again with the invited one. The apps you can use after joining depend on [app permissions](https://keelson.dev/docs/workspace/groups-and-app-access/). By default, every member can use every app. ## Share the app URL The person sending the invitation should also send the app URL in chat or email. Invitation emails do not include app URLs. App Users cannot access the console's app list or management pages. After joining, they see a message directing them to ask the person who invited them for an app URL. ## Manage invitations On the **Members** page, people who have not accepted their invitation appear as **Pending**; those who have joined appear as **Active**. - **Revoke** invalidates an invitation that has not been accepted. - **Resend** issues a new link for an invitation that has not been accepted. The old link becomes invalid, and the new one expires 3 days (72 hours) after resending. - For an expired or revoked invitation, create a new invitation for the same address. ## Troubleshooting | Problem | What to check | |---|---| | You cannot create an invitation | Are you an Owner or Admin? Has the person already joined? For Developer, Admin, or Owner invitations, is a seat available? | | The email does not arrive | Check spam. If it is still missing, copy the link from the results screen and share it. If that screen is closed, use **Resend invitation** to issue a new link | | The recipient cannot accept | Are they signed in with the invited address? Has the invitation expired or been revoked? | | The person joined but cannot open an app | Check app view permission, the signed-in account, and IP restrictions | ## Other ways to join To let people with your company's email domain join without individual invitations, configure [Domains and Join Policies](https://keelson.dev/docs/workspace/domain-join/) (Team and above). ## Related pages - [Members and Roles](https://keelson.dev/docs/access-control/members-and-permissions/) - [Plans and Limits](https://keelson.dev/docs/workspace/plans-and-limits/) — Developers seats --- # Groups and app permissions Choose who can use and manage each app. URL: https://keelson.dev/docs/workspace/groups-and-app-access/ Language: en --- Use groups to share apps with the right people: an accounting app with Accounting, or company announcements with everyone. You can separately choose who can update an app and change its settings. - **Required permissions / plan:** Owners and Admins can create groups and change membership. Anyone with `manage` permission for an app can change its permissions. Available on all plans. - **Done when:** An account with permission can open the app, and an account with neither view nor manage permission sees an access denied page. For a walkthrough, see [Share an App with Your Team](https://keelson.dev/docs/access-control/share-with-team/). ## Two types of permission | Permission | What it allows | |---|---| | **View (`view`)** | Open and use the running app | | **Manage (`manage`)** | Deploy the app, change settings, edit secrets, and change permissions. Includes view access | Permissions are assigned to **groups**, not individual users. Changing a group's members also changes who can use or manage the apps assigned to that group. Roles (Owner, Admin, Developer, and App User) determine what a member can do in the workspace. The view and manage permissions above determine app access. However, [IP restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) require an Owner or Admin; app manage permission alone does not allow changes to those settings. ## Default assignments New apps automatically receive these assignments: | Group | Permission | |---|---| | `everyone` (all members) | View | | `developers` (Owners, Admins, and Developers) | Manage | By default, every workspace member can use the app, and members with development roles can manage it. Change these settings only for apps that need narrower access. **To restrict access to specific groups, remove view permission from `everyone`.** Adding an Accounting group alone leaves everyone's access in place. Also, `manage` includes viewing, so members of a manage group (`developers` by default) can open the app even if they are not in a view group. ## Groups Manage groups in **Members → Groups**. **System groups** are assigned automatically by role and cannot be edited. | Key | Members | |---|---| | `owners` | Owners | | `admins` | Admins | | `developers` | Owners, Admins, and Developers | | `everyone` | All members | `admins` does not include Owners. To give both Owners and Admins manage permission, select both `owners` and `admins`. **Custom groups** represent business teams such as Accounting, Sales, or Store Staff. A group's key (an identifier such as `accounting`; Japanese characters are also supported) cannot be changed after creation. Its display name and membership can be changed. A group cannot be deleted while it is assigned to an app. ## Assign permissions to an app Open the app's **Permissions** tab and select its view and manage groups. - At least one manage group is required. You cannot leave the assignment empty or assign only groups with no members. - Removing your own manage permission requires confirmation, even if another manage group has active members. - Saved changes normally take effect within two minutes, including for users who are already signed in. ## Common configurations | Goal | Configuration | |---|---| | An internal tool everyone can use | Keep the defaults | | An app for Accounting and the developers who manage it | View: `accounting`; manage: `developers` | | Everyone can use it, but only Owners and Admins can change settings | View: `everyone`; manage: `owners` and `admins` | ## Business permissions inside the app Rules such as "only supervisors can approve requests" or "each department sees different information" must be implemented inside the app. These determine who can view or change business data, separately from permission to update the app itself. For example, ask your AI agent: "Allow only the Accounting group to view the expense list." After implementation, test what members inside and outside that group can see and do. In your code, check business groups such as departments in `attributes.groups`, and check for `manage` in `X-Keelson-User-App-Perms` for app manage permission. See [Use the Signed-in User's Identity](https://keelson.dev/docs/building-apps/using-identity/). ## How access appears in the app list The app inventory's access column shows **Everyone** when all members have access. For restricted apps, it shows the number of people and the groups with access. ## Configure access with the CLI When asking an AI agent to configure access, specify the group's members and who should use or manage the app. This example shows an Owner or Admin granting view access to Accounting and manage access to Owners and Admins. ```bash keelson groups list keelson groups create accounting --name "Accounting" keelson groups members add accounting alex@example.com sam@example.com keelson access show --app my-app keelson access set --app my-app --view accounting --manage owners --manage admins keelson access set --app my-app --view none # Clear view groups; people with manage permission can still use the app ``` The first commands list groups, create Accounting, and add its members. `access show` displays the app's current assignments; the two `access set` commands apply the assignments shown above. ## Related pages - [Members and Roles](https://keelson.dev/docs/access-control/members-and-permissions/) - [Use the Signed-in User's Identity](https://keelson.dev/docs/building-apps/using-identity/) - [IP Restrictions](https://keelson.dev/docs/workspace/ip-allowlist/) --- # Domains and join policies Let people with your company's email domain join without individual invitations. URL: https://keelson.dev/docs/workspace/domain-join/ Language: en --- For larger organizations, you can let people with a company email address join without sending each person an invitation. Share a join link internally, or ask people to request access from the console's [Join or Create a Workspace page](https://console.keelson.dev/join). **Owners and Admins** configure joining and approve requests. This feature is available on **Team plans and above**. ## Choose how people join | Method | When the person becomes a member | |---|---| | Open a join link shared internally | After signing in with an eligible email address and selecting **Join**. No administrator approval is needed | | Request access from the Join or Create a Workspace page | When an Owner or Admin approves the request | Both methods assign the **App User** role. An Owner or Admin can change it afterward. ## Eligible email addresses Eligibility is determined by the email domain: the part after `@`. For example, the domain of `name@example.com` is `example.com`. - The Owner or Admin configuring this feature must have the same email domain as the workspace creator. - Join links cannot be enabled for public email domains such as Gmail or Outlook.com. - The same domain can be used in multiple workspaces, for example one per department. Enabling a join link automatically registers the email domain of the Owner or Admin who enables it. You do not need to add the domain first. Check registered domains in **Settings → Security → Access Control**. If you add a domain manually, it must also match both your email domain and the workspace creator's. ## Enable and share a join link 1. Open **Settings → Security → Team Join Link**. 2. Enable the join link. 3. Copy the URL and share it in an internal chat or information page. A person with an eligible address opens the link, signs in, and selects **Join** to become a member immediately. Check the **Members** page to confirm they have joined. ## Approve join requests When the join link is enabled, the workspace also appears on the [Join or Create a Workspace page](https://console.keelson.dev/join) for people signed in with the eligible email domain. Selecting **Request to join** sends a request to Owners and Admins. Approve or reject requests under **Pending Join Requests** in **Settings → Security**. Submitting a request does not make someone a member; they cannot access apps until approved. ## Share app URLs after joining A join link is for joining the workspace. Share the URLs of the apps you want people to use separately, because App Users cannot see the console's app list. The apps they can use after joining depend on [app permissions](https://keelson.dev/docs/workspace/groups-and-app-access/). ## Change or disable the link - **Reset link** invalidates the current URL and creates a new one. Use this when you want to change where the link is shared. - Disabling the join link returns the workspace to invitation-only joining. Existing members are not affected. ## Join policies In the configuration details, joining methods are called **join policies**. The policy follows whether the join link is enabled; there is no separate policy selector. | Policy | Behavior | |---|---| | **Invite only** (default) | People join through individual invitations from an Owner or Admin | | **Join link** | People with an eligible email domain can join through the link without approval. Requests from the Join or Create a Workspace page require administrator approval | ## Related pages - [Members and Roles](https://keelson.dev/docs/access-control/members-and-permissions/) - [Invite Members](https://keelson.dev/docs/workspace/invite-members/) - [Plans and Limits](https://keelson.dev/docs/workspace/plans-and-limits/) --- # IP restrictions Allow access only from approved networks. URL: https://keelson.dev/docs/workspace/ip-allowlist/ Language: en --- Restrict an app to your office network or company VPN. In addition to checking login and app permissions, Keelson checks the network a user connects from. This is called IP restriction. It applies to server-side web apps and the API portion of hybrid apps, where page delivery and server-side processing are separate. **It does not apply to static sites, SPAs, static files in hybrid apps, or Media delivery URLs.** See [Scope](#scope) for details. - **Required permissions / plan:** Creating access sources, setting workspace defaults, and configuring individual apps all require Owner or Admin. App `manage` permission alone is not enough. Available on all plans. - **Done when:** The app opens from an allowed network, and other networks receive an access denied page showing the IP address they are connecting from. ## How it works Configure IP restrictions using **access sources**: named collections of allowed network addresses. For example, an access source named "London office" can contain several IP addresses or address ranges. Ranges use CIDR notation, such as `203.0.113.0/24`. 1. Create access sources in the workspace. 2. Choose the workspace's default allowed sources. An empty selection means no IP restrictions. 3. For each app, choose whether to inherit the workspace default or use sources selected specifically for that app. New apps inherit the default. Changing the workspace default updates all apps that inherit it. Apps with their own sources use those settings instead. ## Set up restrictions ### Create an access source (Owner / Admin) In the console, open **Settings → Security → Allowed Sources** and select **+ Add Source**. - **Source name:** For example, "London office" or "VPN". - **IP / CIDR:** One entry per line. Both IPv4 and IPv6 are supported. Enter individual addresses (`203.0.113.10`) or ranges (`203.0.113.0/24`). Use **Add current IP** to include the address you are connecting from. - **Add to workspace default:** Select this to include the new source in the default as soon as it is created. To make an existing source part of the default, select it under **Default Allowed Sources** on the same **Security** page, then select **Save default sources**. Ask your network administrator which fixed IP addresses or ranges to register: "I want this app to be accessible from the office and our company VPN." Home and mobile networks may change your IP address, which can prevent access even after you register it. ### Configure an individual app (Owner / Admin) Open the app's **Settings → Security**. | Mode | Behavior | |---|---| | **Inherit Workspace Default** | Applies the default access sources. If the default is empty, there are no IP restrictions | | **App-specific Sources** | Replaces the default with one or more sources selected for this app | For an app without IP restrictions, configure it to inherit an empty default. **Clearing the default removes IP restrictions from every app that inherits it.** To keep restrictions on other apps, first switch those apps to their own access sources, then clear the default. Changes take effect without a deployment, normally within two minutes. Changes made during an outage take effect after recovery. ## If access is denied When someone opens an app from a network that is not allowed, they see a page after login explaining that the app is available only from allowed access sources. It also shows their current IP address. Check whether you need to connect to the company VPN. If the address needs to be added to an access source, send the displayed IP address to an Owner or Admin. ## Scope - Restrictions apply to signed-in users accessing container-based web apps and the API portion of hybrid apps. - **They do not apply to static sites, SPAs, static files in hybrid apps, or Media delivery URLs (`/__keelson/media/`).** Media checks login and app view permission. See [Files and Media](https://keelson.dev/docs/building-apps/files-and-media/). - App tokens and webhooks use the allowed IPs configured on each token, rather than the workspace or app settings. See [External Integrations](https://keelson.dev/docs/building-apps/external-integrations/). - Tools that AI clients call over MCP are also subject to the same workspace default and per-app settings. The IP address checked is that of the AI client's server. Calls from claude.ai or ChatGPT come from each provider's servers, not from the user's computer, so they are denied as-is for apps with an IP restriction. See [Use your app from AI clients (MCP)](https://keelson.dev/docs/building-apps/mcp/). ## Technical details Checks run in this order: **login → app permissions → IP address**. IP restrictions add a further requirement to login and app permission checks. Requests from disallowed networks return HTTP 403. Browser page navigation receives an explanatory page; requests made within a page (fetch / XHR) and API calls receive JSON. ## Related pages - [Authentication and Login](https://keelson.dev/docs/access-control/auth-and-login/) - [Groups and App Permissions](https://keelson.dev/docs/workspace/groups-and-app-access/) - [Use your app from AI clients (MCP)](https://keelson.dev/docs/building-apps/mcp/) --- # Plans and limits Compare plan limits and understand what happens when you reach them. URL: https://keelson.dev/docs/workspace/plans-and-limits/ Language: en --- Check usage limits when choosing a plan or when you can no longer add or update an app. For prices, see the [pricing page](https://keelson.dev/pricing/). For plan changes, cancellation, and invoices, see [Billing and Subscription Management](https://keelson.dev/docs/workspace/billing/). Start with these three questions when choosing a plan: - **How many apps need to run at once?** This is the number of web apps in use at the same time, not the total number of apps you have saved. - **How many people will build or manage apps (Developers)?** This allowance covers Owners, Admins, and Developers. App Users, who only use apps, do not count. - **How much storage do you need?** This covers saved files and other stored content. Database size is accounted for separately. ## Limits by plan | | Starter | Plus | Team | |---|---|---|---| | Apps running at once | 1 | 2 | 4 | | Developers (Owner / Admin / Developer seats) | 1 | 2 | 3 | | Background Jobs runs per month | 500 | 2,500 | 5,000 | | Background Jobs runtime per month | 300 minutes | 1,500 minutes | 5,000 minutes | | Scheduled job definitions per app | 3 | 5 | 10 | | Minimum interval between scheduled runs | 60 minutes | 15 minutes | 5 minutes | | Background task definitions per app | 10 | 10 | 10 | | Maximum duration per run (scheduled run or background task attempt) | 3 minutes | 5 minutes | 10 minutes | | Builds per month | 100 | 300 | 1,000 | | Build time per month | 500 minutes | 1,500 minutes | 5,000 minutes | | Concurrent builds | 1 | 1 | 2 | | Daily backups retained | 1 | 3 | 7 | | Point-in-time recovery (PITR) window | 24 hours | 7 days | 14 days | | Storage (workspace total) | 10 GB | 20 GB | 50 GB | | Database size (per database) | No fixed limit | No fixed limit | No fixed limit | | Email recipients per month | 500 | 2,000 | 10,000 | | Priority start (paid subscriptions) | — | Yes | Yes | | Custom domains | — | — | Up to 10 | | Custom email sending domains | — | — | Up to 10 (20 allowed sender addresses each) | | Access log history | 30 days | 90 days | 180 days | | App log history | 7 days | 14 days | 30 days | The Enterprise plan is in preparation. [Contact us](https://keelson.dev/contact/) if your requirements exceed these limits. A **build** prepares an app to run. A **scheduled job** runs a task at specified times or intervals, configured using cron. A **background task** runs a command declared in `tasks` in a separate instance when the app enqueues it with the SDK; each started attempt, including a retry, counts as one Background Jobs run. Each retained daily backup is one saved backup version. For email sent with the [Email SDK](https://keelson.dev/docs/building-apps/external-integrations/#send-email), each To, CC, or BCC address counts as one recipient. The limit applies both to the workspace total and to each app's total; sending stops when either is reached. ## Included in every plan - Unlimited App Users. - Unlimited saved apps on paid plans. Only the number that can run at once is capped. Trials have separate limits on owned apps and apps running at once; see [Trial limits](#trial-limits). - Authentication, access control, and IP restrictions. - Daily backups, manual snapshots (5 per app per day), PITR, and backup downloads. - Managed SQLite, with databases isolated by app or workspace. - A 14-day free trial, once per account. Starter and Plus can start without a card. ## Trial limits - You can create up to 3 apps, excluding apps already deployed as static sites. Apps that have not been deployed and apps that use a server count toward this limit. Creating a fourth is rejected. - Up to 3 apps can run at once, regardless of plan. During the trial, this replaces the plan's active-app allowance, and extra-app add-ons do not increase it. For Team, this reduces the allowance from 4 to 3. - You can run up to 7 builds in a rolling 24-hour period. This allowance does not reset all at once at midnight. - You can run up to 30 builds over the entire trial. This total does not reset at the start of a new month. - Build time and concurrent build limits remain those of your selected plan. - When you move to a paid subscription, the owned-app limit is removed, and your active-app allowance becomes the plan allowance plus any extra-app add-ons. ## What happens when you reach a limit | Limit | Behavior | |---|---| | Apps running at once | Apps that need a free slot cannot start or deploy. Running apps are unaffected. Opening a sleeping app when no slot is available shows an explanatory page (HTTP 503). An app without priority start releases its slot about 5 minutes after the last request | | Developers | Invitations that reserve a Developers seat and role changes that use an additional seat are blocked. You can still invite App Users and make role changes that do not increase seat usage | | Background Jobs runs | Remaining scheduled runs for the month are skipped, and new background tasks are rejected (`TASK_MONTHLY_QUOTA_EXCEEDED`). The allowance resets next month | | Background Jobs runtime | Remaining scheduled runs for the month are skipped, and new background tasks are rejected (`TASK_MONTHLY_QUOTA_EXCEEDED`). A run or attempt that has already started is allowed to finish. The allowance resets next month | | Scheduled job definitions, intervals, and duration; background task definitions and duration | Deployment is blocked if `keelson.yaml` exceeds your plan's limits. Ask your AI agent to adjust the settings to the limit shown | | Monthly builds / build time | New builds are rejected. Both allowances reset next month. The trial's 30-build lifetime limit does not reset; moving to a paid plan is required to continue | | Concurrent builds | New builds are rejected until a running build finishes and capacity becomes available | | Email recipients | Further sends for the month are rejected (`MONTHLY_QUOTA_EXCEEDED`). The allowance resets next month | | Custom email sending domains | Adding another domain or allowed sender address is rejected. Remove one you no longer use first | | Storage | Apps do not stop immediately. Support will contact you about the overage | | Database size | There is no fixed per-plan limit, but underlying technical limits still apply. App execution may be restricted or suspended if usage significantly exceeds normal business workloads or affects platform stability, other customers, or our reasonable operating costs. Emergency restrictions may be applied without prior notice | ## Add-ons (Team and above) During a paid subscription, you can add capacity as needed. See [Billing and Subscription Management](https://keelson.dev/docs/workspace/billing/) for how to make changes and how they are billed. Add-ons are not available during the trial. | Add-on | Unit | |---|---| | Extra storage | +50 GB | | Extra app running at once | +1 slot | | Extra Developer | +1 seat | ## Terms ### Apps running at once This is the number of slots web apps reserve to start and run. An ordinary app automatically releases its slot about 5 minutes after the last request. It keeps using the slot while requests continue. - Sleeping and suspended apps do not use a slot. - Apps with only scheduled jobs do not use a slot; jobs run separately from web apps. - Static sites do not use slots, and there is no limit on how many you can create. - Apps with **priority start** reserve a slot even without traffic. On paid subscriptions, this is available on Plus and above, for up to one fewer app than the total app-slot allowance. During the trial, all plans can enable it for up to 2 apps. See [When Apps Run](https://keelson.dev/docs/building-apps/how-apps-run/) for how apps start and stop. ### Developers **Developers seats** are the allowance for people who build and manage apps. Each Owner, Admin, or Developer uses one seat. App Users do not count. An unexpired invitation for an Owner, Admin, or Developer reserves one seat and counts toward usage even before acceptance. Revoke invitations you no longer need to release those reservations.
How seat counting differs by operation When sending a new Owner, Admin, or Developer invitation, the limit is checked against existing members with these roles plus unexpired, unaccepted invitations. When accepting an invitation or changing a role to use an additional Developers seat, only existing members with these roles are counted. Changes that do not increase seat usage, such as changing the role of someone already using a seat, are not blocked by this limit.
### Storage Storage is the combined size of saved deployment files (artifacts), [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) files, and snapshots. It is measured across the workspace and excludes logs. Managed SQLite size is accounted for separately and has no fixed per-plan limit. ## HTTP time limits Each request to your app, such as loading a page or saving data, has a time limit. If your app needs to do lengthy processing, ask your AI agent how to complete it within these limits. | Item | Limit | |---|---| | Time until the response starts (first response headers) | 120 seconds; exceeding this returns a timeout (HTTP 504) | | Total time for one request, including streaming responses | 300 seconds | | Time between chunks in a streaming response | 120 seconds | Even if a timeout appears, the app may have completed an operation such as saving data. Ask your AI agent: "Make sure repeating the same action does not create duplicate requests or orders." Technically, an HTTP 504 does not necessarily mean the app's processing was canceled. Keelson does not automatically retry `POST` requests, but users or clients may retry them. Your implementation therefore needs to avoid duplicate results when the same request is repeated. For scheduled run and background task duration limits, see the plan table above. For build time limits, see the [keelson.yaml Reference](https://keelson.dev/docs/reference/keelson-yaml-reference/#dependency-installation). ## Related pages - [Scheduled Jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) - [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) - [Database (Managed SQLite)](https://keelson.dev/docs/building-apps/persistent-storage/) - [Invite Members](https://keelson.dev/docs/workspace/invite-members/) --- # Billing and subscriptions Manage plans, trials, cancellations, invoices, workspace deletion, and data exports. URL: https://keelson.dev/docs/workspace/billing/ Language: en --- Use this guide to change your plan, check payment details and invoices, or end your use of Keelson. See [Plans and Limits](https://keelson.dev/docs/workspace/plans-and-limits/) for usage limits and the [pricing page](https://keelson.dev/pricing/) for prices. - **Required permissions:** Owner or Admin. Only Owners can delete a workspace. - **Where:** **Settings → Billing & Usage** in the console. Workspace deletion is under **Settings → Danger Zone**. ## When billing starts - Each account can use one 14-day free trial. 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 to start the trial; cancel during the trial to avoid being charged. - If a card is registered, your first charge is on the trial's end date. Subsequent charges occur monthly on your subscription's billing date. - If an account that has already used its trial creates a second workspace, billing starts as soon as checkout is complete. - Currency is determined by the billing country: JPY including tax for Japan, and USD excluding tax elsewhere. It is fixed at the first subscription; contact support to change it later. - Payment on invoice and bank transfers are not supported. The number of App Users does not affect your bill. ## Trial limits and what happens when a trial ends All trial plans have shared limits on owned apps, apps running at once, and builds. See [Trial limits](https://keelson.dev/docs/workspace/plans-and-limits/#trial-limits) for the numbers and how usage is counted. | Status at trial end | What happens | |---|---| | Card registered | The subscription becomes paid and the first charge is made. The owned-app limit is removed, and the active-app allowance returns to the plan's allowance | | No card registered (Starter / Plus) | The trial ends automatically without a charge. New deployments, app starts, and scheduled runs are blocked. Apps and data are not deleted immediately; see [Apps and data after your subscription ends](#apps-and-data-after-your-subscription-ends) | During the trial, both upgrades and downgrades take effect immediately without a charge. ## Change your plan Open **Billing & Usage → Change plan**. The amount is shown before you confirm. | Change | Takes effect | Billing | |---|---|---| | Upgrade | Immediately | Credit for the unused portion of the current plan is deducted from the charge for the remaining period on the new plan. This difference is charged when you change. The full new price applies from the next billing date | | Downgrade | At the end of the current billing period | No prorated adjustment. You keep the current plan until the change takes effect | A downgrade appears as a scheduled change. You can cancel it before it takes effect. A warning appears if current usage exceeds the new plan's limits. If usage still exceeds them when the plan changes: - **Apps running at once:** Sleeping apps cannot start until usage falls within the allowance. - **Developers seats:** Invitations for Developers, Admins, or Owners, and promotions from App User to these roles, are blocked until usage is within the limit. - **Scheduled runs:** Remaining runs for the month are skipped. - **Storage:** This is a soft limit: apps do not stop immediately. A deployment warning appears and support contacts you. Sustained, substantial overages may lead to restrictions. - **Custom domains (Team only):** Existing registrations remain, but you cannot add or re-register domains. ## Change add-ons (paid Team subscriptions and above) Under **Billing & Usage → Addons**, choose quantities for extra app slots (+1), Developers seats (+1), and storage (+50 GB). **Add-ons cannot be changed during the trial**; you can add them after it ends. Increases are charged immediately to your registered payment method, prorated for the remaining subscription period. Reductions are credited toward future invoices. ## Payment methods and invoices Recent invoices appear under **Invoices** in **Billing & Usage**. Each row links to the Stripe invoice page and PDF. To change your payment method, edit billing details, or see older invoices, select **Manage billing**. This opens the Stripe customer portal in another tab. You can cancel there too, but canceling from the Keelson console lets you review the end date and its impact before proceeding. If a payment fails and becomes overdue, the service continues normally while Stripe retries. If payment still fails after retries end, the subscription is suspended and new deployments, app starts, and scheduled runs are blocked. Use **Update payment method** to update your details. ## Export your data If you are leaving Keelson, **export the data you need before your subscription ends**. If deleting an app or workspace, export before deletion. After requesting cancellation, you can continue using the service until the subscription end date, giving you time to prepare. | Data | How to export it | |---|---| | Managed SQLite data | On the app page, open **Restore points**, choose a point within the recovery window, and download a SQLite dump. Downloads remain available during the grace period after the subscription ends | | [Files SDK](https://keelson.dev/docs/building-apps/sdk/) files | Add file listing and download functionality to your app, and download files while the app can still run | | [Media SDK](https://keelson.dev/docs/building-apps/sdk/) files | Create links using the file IDs saved by your app. A member with app view permission can sign in and download the files | | Source code | From Git, or download it from an individual deployment in the **Deploys** tab | | Environment variables and secrets | Secret values are not displayed in the console. If needed, retrieve them from where you originally managed them | | Access logs and app logs | Export from the **Logs** tab (up to 5,000 lines for app logs) | The console and CLI do not provide file listing or downloads for Files / Media. You can ask your AI agent: "Build a page where I can list and export saved files. Implement downloads for Files and create links from saved IDs for Media. Allow only people with view permission for the app to use it." Check that the downloaded files open correctly. See [Files and Media](https://keelson.dev/docs/building-apps/files-and-media/) for how these services differ. Because apps cannot start after the subscription ends, **finish exporting Files / Media before that date**. ## Cancel your subscription Open **Billing & Usage → Cancel plan**, review the end date and impact, and select **Confirm cancel**. - **The plan remains active until the subscription end date and time, at the end of the current billing period.** You can keep using it until then after requesting cancellation. There is no refund for the remaining period. - Before that date and time, select **Reactivate** to undo the cancellation request. - After that date and time, new deployments, app starts, and scheduled runs are blocked. Apps and data are not deleted immediately; you receive a deletion notice and grace period. ## Apps and data after your subscription ends If your subscription ends after cancellation, your trial ends without a card, or your subscription is suspended for nonpayment, workspace apps can no longer start. They do not disappear immediately. 1. When eligible apps are identified, Owners and Admins receive a **deletion notice** in the console and by email. It includes the scheduled deletion date. 2. The grace period is 3 days for a workspace that has never made a payment, or 30 days for one with payment history. Service restrictions remain during this period, but you can sign in to the console and download database backups. 3. If the subscription becomes active during the grace period, the notice is canceled and the apps are kept. If the subscription has ended, select a plan to subscribe again. If it is suspended for nonpayment, use **Update payment method** to settle the outstanding balance. Complete this before the grace period ends to keep the apps. 4. After the grace period, apps are deleted automatically. As with user-approved deletion, database backups, Files, and a record of settings without secrets are retained separately from normal service use for 7 days. Restoration is not guaranteed. Database backups can still be exported during the grace period. Files / Media must be exported before the subscription ends; see [Export your data](#export-your-data). Check the notice for the scheduled deletion date. See also [Article 20 of the Terms of Service](https://keelson.dev/terms/) for data handling after termination and exporting before your subscription ends. ## Delete a workspace Open **Settings → Danger Zone → Delete this workspace**. Only an Owner can do this, and you must enter the workspace name to confirm. - The workspace and all its apps are deleted, and members lose access. Deletion cannot be undone from the console. - Each app's database backups, Files, and a record of settings without secrets are retained separately from normal service use for 7 days. Restoration is not guaranteed. - Deletion signs you out. - If a subscription is active, deletion also attempts to cancel it; you do not need to cancel first. However, cancellation can fail. After deletion, check that billing has stopped using the Stripe portal (open it through **Manage billing** before deleting) or invoice emails. To delete your own Keelson account, open the account menu at the top right of the console, then **Account settings → Security → Delete account**. This deletes your Keelson account, not the Google or Microsoft account used to sign in. Apps and data in workspaces you belonged to remain. ## Related pages - [Plans and Limits](https://keelson.dev/docs/workspace/plans-and-limits/) — limits and what happens when you reach them - [Update and Restore](https://keelson.dev/docs/deploy/update-and-restore/) — app deletion and what to export first - [Pricing](https://keelson.dev/pricing/) # Reference --- # Keelson Deploy Spec The canonical specification of supported runtimes, constraints, and requirements for deploying apps to Keelson URL: https://keelson.dev/docs/reference/deploy-spec/ Language: en --- **Spec version:** 2026-10-06 / **Raw text (for AI agents):** [`/docs/reference/deploy-spec.txt`](https://keelson.dev/docs/reference/deploy-spec.txt) 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](https://keelson.dev/docs/deploy/deploy/). --- ## Definition of a successful deployment Verify deployment completion according to the app type, as well as the build result. | App type | Conditions to verify | |---|---| | Web app or hybrid | The build is complete, the web app process has started, and the health check has passed. An app URL has been issued and is reachable. A hybrid app must also serve its static files | | Static site or SPA | Files have been uploaded and published and can be served at the app URL. No app process startup or web health check is required | | Scheduled jobs only | The build is complete, and the schedules and job execution environment settings have been applied. Web process startup, health checks, and URL access do not apply | For web apps, a successful build is insufficient if startup or the health check fails. The app URL is `https://--.keelson.run`. Scheduled jobs run on their configured schedules after deployment. A successful deployment does not establish that individual jobs have completed successfully; check their execution history and logs. --- ## Required file Every deployment requires a `keelson.yaml` file in the project root directory. Minimal configuration: ```yaml slug: my-app runtime: python-slim command: "python app.py" db: mode: none ``` `db` and `db.mode` (`libsql` or `none`) are required. Framework-based apps such as Flask require a production server. See [Production hardening](#production-hardening). See the [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) for details about every field. --- ## Placement region 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. **Only `jp-tokyo` is currently open for new applications**; `us-oregon` is defined but not yet offered. For a new application, select a region with `keelson deploy --new --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. An explicit selection of a region that is not yet offered (currently `us-oregon`) is rejected rather than redirected. Without a selection the app is placed in the workspace's default region (Japan), so do not set `region` unless the user asks for it. 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. --- ## Supported runtimes 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. ### Difference between slim and media - **slim** — Contains the language runtime and standard libraries only. It builds faster and has a smaller image. - **media** — Adds common system libraries required for image processing, such as Pillow and sharp, and for video processing. ### Supported frameworks Keelson does not depend on a specific framework. An app can run if its `command` starts it and it accepts requests as an HTTP server. Examples include FastAPI, Flask, Express, Next.js, Hono, and Gin. --- ## Unsupported runtimes The following languages and runtimes are not supported: - Ruby - Java / Kotlin / Scala - PHP - Rust - .NET / C# - Elixir / Erlang - Swift Apps that require an unsupported runtime cannot be deployed to Keelson, even after configuration changes that do not replace that runtime. --- ## Build and runtime constraints Keelson provides fixed build and runtime environments. The app must be able to build and start in those environments. ### Operating system and architecture - **OS:** Linux - **CPU:** x86_64 (amd64) ### Root privileges Apps run as a non-root user. They cannot use `sudo`, run `apt-get install`, or make system-level changes. ### Dockerfile A Dockerfile in the repository is ignored; it is not a reason to refuse a deploy. Keelson selects a runtime and starts the app with `command`. Specify the runtime and start command in `keelson.yaml` instead of a Dockerfile. ### File system | Path | Writable | Persistent | Purpose | |---|---|---|---| | `/data` | No; it cannot be created by your app | No | Keelson does not provide it. Apps run as UID 1000 and cannot create directories under the root-owned `/` | | App directory (`/workspace`) | Cannot create entries directly in this directory; existing files and subdirectories may be writable depending on permissions | No | Source code and dependencies | | `/tmp` | Yes, temporarily | No | Temporary files | | Home directory (`/home/appuser`) | Yes in Python and Node.js runtimes | No | Runtime and tool caches and configuration | | Other paths | Depends on ownership and permissions | No | Use `/tmp` for temporary files | - Use `/tmp` for temporary files. Local writes are not persistent and are lost when the instance restarts or stops. - Keelson provides three forms of persistent storage: Managed SQLite (`db.mode: libsql`), the [Files SDK](https://keelson.dev/docs/building-apps/sdk/) for private app files, and the [Media SDK](https://keelson.dev/docs/building-apps/sdk/) for images and attachments served to users. None is available as a local file path. Apps may also connect to external databases or storage. - File-based SQLite is not persistent. File SQLite clients are detected during deployment and rejected unless declared for ephemeral use with `db.local_sqlite`. ### Port - A web app must listen for HTTP requests on the port specified by the `PORT` environment variable. - It must listen on `0.0.0.0`. Requests cannot reach a server bound to `127.0.0.1` or `localhost`. - Keelson terminates HTTPS. The app itself must listen over HTTP. ### System packages Keelson does not provide an environment where arbitrary OS packages can be added. - `-slim` runtimes contain only a minimal set of system libraries. - `-media` runtimes contain common libraries required for image and video processing. - An app may not work if it needs a system library that is not present in its runtime. - Packages cannot be added with `apt-get` or similar tools because the app runs as a non-root user. ### Process model - The normal model is one process started by `command`. - systemd and daemon managers are not available. - Use `crons` for work that runs at set times, and `tasks` for work that continues after a request ([Background tasks](https://keelson.dev/docs/building-apps/background-tasks/)). `workers` has been removed, and declaring it causes the deployment to be rejected. --- ## Dependency constraints Even when the language runtime is supported, dependencies and system requirements can prevent an app from being deployed. ### Dependencies installed by language package managers Pure language packages managed by the following package managers can be installed normally: - **Python:** pip (`requirements.txt`) - **Node.js:** npm (`package.json`) - **Go:** go mod (`go.mod`) Keelson installs dependencies automatically while building the image by detecting `requirements.txt`, `pyproject.toml`, `package.json`, or `go.mod`. Use `command` only to start the app. If it includes `pip install`, `npm install`, or `go build`, the CLI preflight check stops deployment with `command_installs_dependencies`. Build constraints: `package-lock.json` is the only supported Node lockfile. Only public registries are supported; private registries and authenticated installs are not. Go uses `CGO_ENABLED=0`. The build target is `linux/amd64`. Secrets are unavailable during builds. The time limit is 600 seconds. ```yaml # Python command: "python app.py" # Node.js command: "npm start" # Go (Keelson builds `./app` during deployment) command: "./app" ``` ### Packages with native dependencies Some packages require C libraries or other system-level dependencies. - **Commonly supported by `-media` runtimes:** General media-processing libraries such as Pillow, opencv-python, sharp, and ffmpeg-related packages. - **Potentially unsupported:** Packages that depend on a system library not included in the runtime. ### Common unsupported patterns | Pattern | Reason | |---|---| | Requires `apt-get install` | Packages cannot be added without root privileges | | Depends on a specialized C library | The library may not be included in the runtime | | Requires a GPU inference library | GPU instances are not provided | | Runs a database server such as PostgreSQL, MySQL, or Redis | The app can connect to an external service, but cannot run that server on Keelson | | Requires systemd or a background daemon | The process model is different | --- ## Deployment modes 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. | Name | Raw `deploy_mode` | `command` | `assets` | Description | |---|---|---|---|---| | Web app | `container` | Present | Absent | Standard app deployment | | Static site | `edge-static` | Absent | Present, without `fallback` | Static files only | | SPA | `edge-spa` | Absent | Present, with `fallback` | Single-page app with fallback routing | | Hybrid | `hybrid` | Present | Present; `fallback` required when `type: web` is explicit | Static files plus a backend API | | Scheduled jobs only | `container` | Absent | Absent | Requires `crons`; runs scheduled jobs without starting a web server | Apps that run only scheduled jobs also use `deploy_mode: container`, but their startup and completion checks differ from web apps. Static sites and SPAs require `type: web` and cannot use `crons`. --- ## Reserved URL paths Some paths on the app's host are used internally by Keelson or blocked from external access. Do not use the following as public app endpoints. - **`/__keelson` and all paths beneath it are reserved for platform internals for every HTTP method.** Keelson uses them for platform-served assets, file downloads, and internal endpoints. Do not define app routes at these paths. - **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. - **The platform responds to `GET /health` and `GET /_health` through the gateway.** These GET requests do not reach the app even if it defines matching routes. - **`/api/webhooks/email` and `/api/webhooks/email-events` are blocked from external access at the edge.** Use different paths for app-defined webhooks. - **`/api/mcp` and paths beginning with `/api/mcp/` are reserved for MCP tool calls.** In apps that run server-side code (including the server side of a hybrid app), requests to these paths that do not come through MCP return 404, even if `keelson.yaml` has no `mcp:` block. Do not put any route other than tool handlers (`POST /api/mcp/`) here. Statically served parts are not affected. - **In apps with `mcp.enabled: true`, Keelson responds to `/mcp`, `/.well-known/oauth-protected-resource`, and `/.well-known/oauth-protected-resource/mcp`.** These requests do not reach the app. See [Use your app from AI clients (MCP)](https://keelson.dev/docs/building-apps/mcp/). - 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`. Paths such as `/assets/*` (Vite's default output), `/files/*`, `/static/*`, and `/uploads/*` are available to apps. `/api/*` is also available subject to the exceptions above and the authentication configuration rules. ### Framework static-path collision matrix (reference) The following table lists common frameworks' default static paths. “Verified” means delivery has been checked in this repository; “Needs verification” means the result is inferred from default paths. For unverified frameworks, check the actual build output and delivery. This table is only about path collisions with `/__keelson`; it does not indicate whether a framework is supported. | Framework | Default static path | Conflicts with `/__keelson` | Status | |---|---|---|---| | Vite (Vue / Svelte / React / Solid / Preact) | `/assets/*` | No | Verified | | Remix v2 / VitePress | `/assets/*` | Expected not to | Needs verification | | Angular | `/assets/*` | Expected not to | Needs verification | | Next.js | `/_next/static/*` | Expected not to | Needs verification | | Nuxt / SvelteKit / Astro | `/_nuxt/*` · `/_app/*` · `/_astro/*` | Expected not to | Needs verification | | CRA / Django / Flask | `/static/*` | Expected not to | Needs verification | --- ## Environment variables and secrets Configure API keys, tokens, and connection details required at startup as environment variables or secrets instead of embedding them in source code. - `env` in `keelson.yaml` — Values that are safe to include in version control. - Console secrets — API keys, tokens, and other values that should not be committed to source code. The app may fail to start correctly if a required value is missing. See [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) for details. ### Environment variables set by Keelson | Variable | Description | |---|---| | `PORT` | Port the app must listen on. Read it; do not set it | | `TZ` | Workspace time zone, detected automatically from the browser when the workspace is created | | `KEELSON_MODE` | Marker indicating that the app is running on Keelson (`keelson`) | | `KEELSON_APP_ID` | Internal app 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 deploy | The former tenant-named variable remains accepted as a compatibility alias. No removal date is set. With `db.mode: libsql`, Keelson also injects `KEELSON_DB_URL` / `KEELSON_DB_AUTH_TOKEN` (aliases: `TURSO_DATABASE_URL` / `TURSO_AUTH_TOKEN`). Names beginning with `KEELSON_` are reserved and cannot be used in `env` or `secrets`. --- ## Production hardening 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. ### Production server by framework **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: ```yaml command: "gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 --graceful-timeout 9 app:app" ``` 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. **Django:** Start Django with gunicorn: ```yaml command: "gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 --graceful-timeout 9 config.wsgi:application" ``` - Keep `DEBUG` off by default. - 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: ```yaml command: "uvicorn main:app --host 0.0.0.0 --port $PORT" ``` 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: ```yaml command: "npm run start" ``` Set the package script to: ```json "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: ```yaml 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. ### Debug and development flags Defaults must be safe for production. In Python, default `DEBUG` to false: ```python DEBUG = os.environ.get("DEBUG", "false").lower() == "true" ``` Do not default it to true: ```python DEBUG = os.environ.get("DEBUG", "true").lower() == "true" ``` Debug pages can expose environment variables, including database authentication tokens. ### Detecting Keelson at runtime Use `KEELSON_MODE`, which Keelson sets to `keelson`, to detect the platform: ```python ON_KEELSON = os.environ.get("KEELSON_MODE") == "keelson" ``` 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. --- ## Timeouts | Target | Limit | |---|---| | HTTP request | The response must start within 120 seconds or the request returns 504. Once streaming starts, the limits are 120 seconds without data and 300 seconds total | | Scheduled job (cron) | 1–600 seconds; default 300 seconds, reduced to the plan limit if it is lower | | Build | 600 seconds | --- ## Outbound connections - Apps can connect to external APIs and services by default. - The only exception is the SMTP ports (TCP 25, 465, 587, and 2525), which are always blocked. Send email through an email delivery service's HTTPS API instead. --- ## Common failure patterns | Symptom | Cause | Resolution | |---|---|---| | App is running but unreachable, or health checks fail | 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 | | Preflight stops with `command_installs_dependencies` | `command` includes `pip install` / `npm install` | Remove installation steps and keep only the start command | | Validation stops because `db` is missing | No `db.mode` is declared | Declare `libsql` or `none` | | Build fails with `reserved_path_conflict` | A `__keelson` directory exists at the root of the served build output | Rename the directory. See [Reserved URL paths](#reserved-url-paths) for other restrictions | | 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 | Work deferred until after the response did not run | Finish it before responding, or enqueue it as a [background task](https://keelson.dev/docs/building-apps/background-tasks/). Without background tasks, store the work in the database and process it periodically with `crons`; tell users it waits until the next scheduled run and may take longer depending on execution conditions | ### Troubleshooting steps 1. **Check the build logs** — Confirm that dependencies were installed successfully. 2. **Check the runtime logs** — Look for startup and runtime errors. 3. **Check `keelson.yaml`** — Verify `runtime`, `command`, and `env`. --- ## Expected behavior for AI agents When deploying an app to Keelson, an AI agent should make decisions in the following order. ### Decision flow 1. **Check the runtime** — Confirm that the app's language appears in this specification's supported runtimes. 2. **Check dependencies** — Confirm that native dependencies and system-library requirements fit within the constraints. 3. **Check `keelson.yaml`** — Confirm that the file exists and its configuration is valid. Create it if it is missing. 4. **Check environment variables** — Identify any missing environment variables or secrets the app requires. 5. **Deploy** — Use the CLI or Skill to run the deployment. 6. **Verify completion** — Follow the [definition of a successful deployment](#definition-of-a-successful-deployment) for the app type. Check startup, health checks, and URL access for web apps; delivery at the URL for static sites and SPAs; and applied configuration for apps that run only scheduled jobs. 7. **Handle failure** — Inspect the logs and make changes where possible. If a constraint is violated, state the reason and stop. ### Mandatory rules - Do not attempt a deployment when the runtime is unsupported. State the reason and stop. - When required configuration is missing, explain what is needed. - Do not treat build success alone as deployment completion. - Always inspect the logs after a startup failure. ### Specification precedence 1. The latest Keelson Deploy Spec on the web (this document) 2. The latest CLI version information on the web 3. The copy of the specification bundled with a Skill 4. General knowledge and assumptions If the web canonical specification conflicts with information bundled with a Skill, follow the web canonical specification. --- ## Related documentation - [Deploy an app](https://keelson.dev/docs/deploy/deploy/) — Deployment steps and lifecycle - [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) — How to manage API keys and configuration values - [App URL](https://keelson.dev/docs/deploy/public-url/) — The URL issued after deployment - [keelson.yaml reference](https://keelson.dev/docs/reference/keelson-yaml-reference/) — Definitions of every configuration field - [Framework considerations](https://keelson.dev/docs/building-apps/frameworks/) — How to start your framework in production --- # keelson.yaml Complete reference for keelson.yaml fields, types, defaults, and constraints URL: https://keelson.dev/docs/reference/keelson-yaml-reference/ Language: en --- ## Overview `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](https://keelson.dev/docs/building-apps/keelson-yaml/). ### Minimal example ```yaml slug: my-app runtime: python-slim command: "python app.py" db: mode: none ``` --- ## Top-level fields | Field | Type | Required | Default | Description | |---|---|---|---|---| | `slug` | string | Yes | — | 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 | | `icon` | string | No | `null` | Image shown as the app's icon in the console; a path relative to `keelson.yaml`. See [icon](#icon) | | `type` | string | Conditional | `null` | App type; only `web` is accepted. Required for static sites and SPAs without a start command | | `runtime` | string | Yes | — | Execution environment; see [Supported runtimes](#supported-runtimes) | | `command` | string \| list | Conditional | — | Start command; optional for scheduled jobs only, or static sites with `type: web` and `assets` | | `env` | map | No | `{}` | Non-secret environment variables | | `db` | object | Yes | — | Database and local SQLite policy | | `crons` | list | Conditional | `[]` | Scheduled jobs; required without `command` unless this is a static site with `type: web` and `assets` | | `tasks` | list | No | `[]` | Background tasks that the app enqueues with the SDK; see [tasks](#tasks) | | `mcp` | object | No | `null` | Tools that AI clients use over MCP; see [mcp](#mcp) | | `storage` | object | No | `{}` | Compatibility block that accepts only the deprecated `disk_id` field | | `assets` | object | No | `null` | Static asset delivery configuration | | `health` | object | No | `null` | Health check path used during deployment | | `secrets` | object | No | `{}` | Secret declarations and requirements | | `auth` | object | No | `null` | Endpoints that bypass interactive Keelson authentication | | `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, `tasks` for work that runs after a request, 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. This includes leading and trailing whitespace and letter case. Unknown top-level keys such as `build`, `port`, and `scaling`, and unknown keys within objects, are rejected. This is not an extensible format for custom fields. `null` is rejected for `crons`, `secrets`, and `storage`; to omit one, leave out the key entirely. ### Writing string values **When in doubt, quote string values.** This ensures the CLI and API read the same string. The following strings require quotes or a block scalar (`|` / `>`). Unquoted values are rejected with `string_value_not_portable`: - Values beginning with a digit, such as `slug: "123-app"`. - Values beginning with a non-ASCII character, such as `description: "Équipe commerciale"`. - Values beginning with `-`, including command arguments: `command: [python, app.py, "--port", "8080"]`. - Words YAML interprets as booleans or null: `yes`, `no`, `on`, `off`, `true`, `false`, `null`, and the case variants in [Environment variable key constraints](#environment-variable-key-constraints). - Values starting with other symbols not covered by the exceptions below, such as `":memory:"`. Quotes may be omitted in the following cases, except for the boolean and null words above: | Value type | Unquoted form allowed | Examples | |---|---|---| | Identifiers such as `slug`, `runtime`, or secret names | Starts with an ASCII letter or `_` | `my-app`, `python-slim`, `OPENAI_API_KEY` | | Commands, descriptions, paths, and other strings | Starts with an ASCII letter, `_`, `/`, `./`, or `../` | `python app.py`, `/health`, `./app` | A colon alone does not require quotes: `description: https://example.com` is valid unquoted. YAML syntax can still require quotes, for example around strings containing `: ` (a colon followed by a space). Quoting does not change field-specific path or length constraints. `env` values are an exception: every value requires quotes or a block scalar, regardless of its first character. See [env](#env). --- ## slug The app identifier. It forms part of the app URL assigned at creation: `https://--.keelson.run`. ```yaml 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. --- ## description An optional one- or two-sentence explanation of who the application is for and what it does. ```yaml description: "Daily and weekly reports for the sales team, with automatic weekly summaries." ``` After trimming surrounding whitespace, the value must be at most 300 characters (Unicode code points). Exceeding this limit fails deployment. The description appears in the console and is applied **only if the app's description is still empty**. Deployment does not overwrite a description edited in the console. --- ## icon An optional image that the console shows as the app's icon in the app list and app header. Apps without an icon show their initial letter, and apps created from a template show the template's icon. ```yaml icon: ./assets/app-icon.png ``` - The path is relative to the directory that contains `keelson.yaml`, not to the directory you run the command from. It must stay inside the project: absolute paths, paths that leave the project with `..`, backslashes, and symbolic links are rejected. - The image is uploaded with your source, so it must not be excluded from the archive (see [Archive contents](#archive-contents)). For a static site without `command`, put it inside `assets.dir`. - Accepted images: PNG, JPEG, or WebP, at most 256 KB, and 32 to 2048 pixels on each side. The format is detected from the file's contents, not its extension. SVG is not accepted. - `keelson deploy` (including `--check`) checks the image before it creates the app or uploads anything, and stops with the path and the reason: `icon.path` (missing, unreadable, symbolic link, or not uploaded), `icon.format`, `icon.size`, `icon.dimensions`, or `icon.corrupt` (the image header cannot be read). The icon is applied when the deployment succeeds, and the CLI prints one `Icon:` line when it finishes (`icon_outcome` in `--ndjson` output): | `icon_outcome` | Meaning | |---|---| | `applied` | The image from `keelson.yaml` is now the app's icon | | `unchanged` | The app already uses this image | | `kept_console_icon` | An icon uploaded in the console is kept; `keelson.yaml` does not replace it | | `kept_cleared` | The icon was removed in the console, so deployments do not add it back | An icon set or removed in the console always takes precedence over `keelson.yaml`. To use the image from `keelson.yaml` after that, upload it in the app's Settings in the console. Removing `icon` from `keelson.yaml` does not remove the app's icon. App icons are being rolled out. If a deployment is rejected with `icon is not available yet`, remove `icon` from `keelson.yaml` and deploy again. --- ## type The app type. Required for static sites and SPAs without a start command; optional otherwise. ```yaml type: web ``` 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`. - Omit `type` for apps that combine a web app and scheduled jobs. --- ## runtime The execution environment. Required even for static-only deployments, but not used to build them. Static sites serve `assets.dir` exactly as built locally or in CI. ### Supported runtimes | Runtime | Language | Intended use | |---|---|---| | `python-slim` | Python | Lightweight applications, APIs, and text processing | | `python-media` | Python | Image and video processing, including Pillow, opencv, and ffmpeg-related libraries | | `node-slim` | Node.js | Lightweight Node.js applications | | `node-media` | Node.js | Image processing, including sharp | | `go-slim` | Go | Lightweight Go applications | | `go-media` | Go | Media-processing Go applications | Start with a `-slim` runtime unless the application needs the extra media libraries included in a `-media` runtime. ### Dependency installation Keelson installs dependencies during the build for container and hybrid deployments. Static sites (`type: web` and `assets`, without `command`) skip this step. By default, only files under `assets.dir` and `keelson.yaml` are uploaded for static sites; see [Archive contents](#archive-contents). Use `command` only to start the app. Including `pip install`, `npm install`, or `go build` causes the CLI preflight check to stop 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]`, **only if `requirements.txt` is absent** | `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: - If both `requirements.txt` and `pyproject.toml` exist, only `requirements.txt` is used. Dependencies and local packages declared only in `pyproject.toml` are not installed; include everything required in `requirements.txt`. - An installable Python `pyproject.toml` must be accompanied by a recognized lockfile: `requirements.txt`, `poetry.lock`, `uv.lock`, or `Pipfile.lock`. - Node.js builds use npm only. Apps declaring external packages, including development, optional, or peer dependencies, require `package-lock.json`. Apps without external dependency declarations may omit it. If `package.json` exists without a lockfile, the build runs `npm install`. If only `pnpm-lock.yaml` or `yarn.lock` exists, preflight stops deployment with `lockfile_unsupported`; these 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. - Go builds use `CGO_ENABLED=0`. - The build target is `linux/amd64` only. - Secrets are not available during the build. - The build execution time limit is 600 seconds. --- ## command The command executed when the application starts. Use either a shell string or an argument list. ```yaml # String form: executed with /bin/sh -c command: "python app.py" # List form: executed directly command: - python - app.py ``` Rules: - Supply `command`, `crons`, or a static site configured with both `type: web` and `assets`. - A cron-only application does not need a top-level `command`. - A static-only `type: web` application with `assets` does not need a top-level `command`. - Do not include dependency installation or build steps. Keelson runs them automatically during the image build. - Empty strings and empty lists mean no start command. An ordinary web app requires a command. `crons[].command` cannot be empty. --- ## env Defines non-secret environment variables as key-value pairs. All values are strings. Keelson sets `PORT` at runtime. A value declared in `env` is ignored. **Use quotes or a block scalar (`|` / `>`) for every value.** An unquoted value is rejected with `env_value_not_string`, and deployment does not start. Numbers and booleans must also be quoted. ```yaml 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. For example, `K: 0o123` can become either `"83"` or `"0o123"`. Quoting guarantees that both paths deploy the same string. ### Environment variable key constraints 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: `yes` `Yes` `YES` `no` `No` `NO` `true` `True` `TRUE` `false` `False` `FALSE` `on` `On` `ON` `off` `Off` `OFF` `null` `Null` `NULL` Quote a key to use a hyphen, a reserved YAML word, or another otherwise unsupported spelling. ```yaml 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. Declare sensitive settings in [secrets](#secrets) and set their values through the console or CLI. When a secret has the same name as an `env` entry, the secret takes precedence; changing that `env` value has no effect. --- ## db Selects how the application handles relational data. The `db` block and its `mode` field are required. ```yaml db: mode: libsql ``` | Field | Type | Required | Default | Description | |---|---|---|---|---| | `mode` | string | Yes | — | `libsql` or `none`; legacy `turso` is normalized to `libsql` | | `migrate` | string | No | `null` | Migration command run during deployment; valid only with `libsql` | | `auto_adopt` | boolean | No | `false` | Accepted for compatibility but currently does nothing; it does not adopt an existing database | | `local_sqlite` | object | No | `null` | Explicit declaration of regenerable, ephemeral local SQLite files | ### Database modes | `mode` | Meaning | |---|---| | `libsql` | Keelson provisions Managed SQLite dedicated to the app and injects `KEELSON_DB_URL` / `KEELSON_DB_AUTH_TOKEN` (aliases: `TURSO_DATABASE_URL` / `TURSO_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. No mode makes file SQLite persistent (`/data` does not exist and cannot be created by the app). Code using file SQLite clients such as `sqlite3` or `better-sqlite3` is detected during deployment and rejected unless `local_sqlite` is declared. ### Database migrations `db.migrate` defines an application-owned migration command. Keelson runs it once against the new image before switching rollout traffic. ```yaml db: mode: libsql migrate: "python migrate.py" ``` - Valid only with `mode: libsql`; use a string. - Runs only for container and hybrid deployments. Static sites have no container, so a declared migration does not run. - Runs in the deployment image after the candidate revision passes its health check and before traffic switches. App startup code must work with the schema before migration. - A nonzero exit code fails the deployment with `deploy.runtime.migration_failed`, and the previous revision remains active. - Runs on every deployment, so make it idempotent, for example with `CREATE TABLE IF NOT EXISTS` and `ON CONFLICT DO NOTHING`. ### Ephemeral local SQLite Use `db.local_sqlite` only for a disposable file that can be regenerated. It documents the application policy; it does not make the file persistent. ```yaml db: mode: none local_sqlite: policy: ephemeral paths: - /tmp/cache.db reason: "A derived cache rebuilt from the source API" ``` | Field | Type | Required | Constraint | |---|---|---|---| | `policy` | string | Yes | Must be `ephemeral` | | `paths` | list of strings | Yes | Non-empty; each entry must be under `/tmp/` or be `:memory:` | | `reason` | string | Yes | Non-empty explanation of why losing the data is safe | The retired `databases` block is always rejected. It cannot be used to persist a file SQLite database. --- ## crons Defines scheduled jobs. Keelson starts each job according to its schedule in a separate instance from the web service. ```yaml crons: - name: cleanup description: "Delete expired sessions" 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 | | `description` | string | No | — | One-line explanation of the job, shown in the console's Scheduled jobs tab; see `crons[].description` below | Rules: - A configuration may contain at most 10 jobs. The plan may impose a lower limit. - Job names must be unique. - `crons[].description` is display only. Leading and trailing whitespace is removed, and an empty value counts as unset. A value longer than 100 characters after trimming is rejected with `cron_description_too_long`; it is not truncated. Changing it does not change what runs. - `type: web` and `crons` cannot be combined. - A job runs in a separate instance; local files, including `/tmp`, are not shared with the web app. Store shared state in `db.mode: libsql`. - 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 absolute timeout limit is 600 seconds. The default is 300 seconds, reduced to the plan limit if that is lower. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for job counts, minimum intervals, and timeout limits by plan. ### Cron expression examples | Expression | Meaning | |---|---| | `0 * * * *` | At minute 0 of every hour | | `*/15 * * * *` | Every 15 minutes | | `0 3 * * *` | Every day at 03:00 | | `0 9 * * 1-5` | Weekdays at 09:00 | | `0 0 1 * *` | The first day of each month at 00:00 | `* * * * *` (every minute) is rejected because it is shorter than every plan's minimum interval. See [Scheduled jobs](https://keelson.dev/docs/building-apps/scheduled-jobs/) for execution behavior and design. --- ## tasks Declares background tasks. When the app's code enqueues a task by name with the SDK, Keelson runs the declared command once in a separate instance from the web service and retries a failed attempt automatically. Unlike `crons`, a task is never started by the clock. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) for enqueueing, writing the command, and making it safe to run twice. ```yaml command: "python app.py" tasks: - name: generate-pdf description: "Generate the invoice PDF for an order" command: "python generate_pdf.py" timeout: 180 max_attempts: 5 ``` | Field | Type | Required | Default | Description | |---|---|---|---|---| | `name` | string | Yes | — | Task name, 1–63 lowercase letters, digits, or hyphens (same rule as `crons[].name`). Leading and trailing whitespace is removed and the name is lowercased, so `Foo` is accepted as `foo` | | `command` | string \| list | Yes | — | Command to execute. A string runs with `/bin/sh -c`; a list runs directly | | `timeout` | integer | No | `300` | Time limit for one attempt in seconds, from 1 through 600 | | `max_attempts` | integer | No | `3` | Maximum number of attempts, including the first, from 1 through 5 | | `description` | string | No | — | One-line explanation of the task, shown in the console's Background tasks tab; see `tasks[].description` below | Rules: - A configuration may contain at most 10 tasks on every plan. - Task names must be unique, and a task cannot have the same name as a job in `crons` (`task_name_conflicts_with_cron`). Names are compared after removing surrounding whitespace and lowercasing. - A configuration with tasks but neither `command` nor `crons` is rejected with `tasks_require_command_or_crons`, because no code could enqueue the tasks. This includes a static site with `type: web` and `assets` but no `command`. - `type: web` and `tasks` can be combined. - `tasks[].description` is display only. Leading and trailing whitespace is removed, and an empty value counts as unset. A value longer than 100 characters after trimming is rejected with `task_description_too_long`; it is not truncated. Changing it does not change what runs. - Unknown keys are rejected. Concurrency cannot be declared; Keelson applies the plan's value. - The absolute timeout limit is 600 seconds. An explicit `timeout` above the plan limit is rejected with `plan_task_timeout`; the default of 300 seconds is reduced to the plan limit if that is lower. - Each attempt receives one line of JSON on standard input with `attempt_no`, `payload`, `schema_version`, `task_id`, and `task_name`. Exit code 0 means success. A task runs in a separate instance; local files are not shared with the web app. See [Plans and limits](https://keelson.dev/docs/workspace/plans-and-limits/) for timeout limits by plan. --- ## mcp Declares the app's tools so that AI clients such as Claude and ChatGPT can use the app over MCP. When a tool is called, Keelson calls the app's `POST /api/mcp/`. Keelson handles login, the consent screen, and the exchange with the AI client. See [Use your app from AI clients (MCP)](https://keelson.dev/docs/building-apps/mcp/) for writing handlers, connecting, and limits. ```yaml command: "python app.py" mcp: enabled: true instructions: "Look up stock levels and record incoming deliveries." tools: - name: search_items description: "Search stock by item name." access: read input: type: object properties: query: type: string required: [query] - name: create_arrival description: "Record one incoming delivery." access: write permission: manage input: type: object properties: item_id: type: string quantity: type: integer minimum: 1 required: [item_id, quantity] ``` | Field | Type | Required | Default | Description | |---|---|---|---|---| | `enabled` | boolean | Yes | — | `true` publishes the tools. `false` keeps the declaration but stops calls (existing connections are not revoked) | | `instructions` | string | No | `null` | Description of the tools as a whole, passed to the AI client. Up to 4096 characters | | `tools` | list | Conditional | — | The tools. Required when `enabled: true`. When written, 1–40 entries regardless of `enabled` | | `file` | string | No | — | Separate file holding `instructions` and `tools`; see [mcp.file](#mcpfile). Requires a recent Keelson CLI | | `tools[].name` | string | Yes | — | Tool name matching `^[a-z][a-z0-9_]{0,63}$` (starts with a lowercase letter; lowercase letters, digits, and `_` only; not normalized). Must be unique. The handler path is `POST /api/mcp/` | | `tools[].description` | string | Yes | — | Description shown to the AI client. 1–1024 characters, not only whitespace | | `tools[].access` | string | Yes | — | `read` or `write`. `write` tools are shown separately on the consent screen, and users can approve read-only access | | `tools[].permission` | string | No | `view` | `view` or `manage`. A `manage` tool can be used only by people with the app's manage permission | | `tools[].input` | object | Yes | — | JSON Schema (2020-12) for the arguments. The top level must be `type: object` | Rules: - Only apps that run server-side code (with `command` and without `assets`) can declare `mcp`. Static sites, SPAs, and hybrid apps are rejected with `mcp is only supported for container apps`. - Unknown keys under `mcp` and `tools[]` are rejected. The handler path is fixed, so `tools[].endpoint` cannot be written. - Write `input` with JSON-compatible values only: at most 32 levels deep and 64 KB as JSON. Quote dates and any key that would otherwise not be read as a string (`yes`, `on`, numbers, and so on). - Do not use `$ref`, `$dynamicRef`, `$recursiveRef`, or `x-mcp-header` in `input`. `$ref` and `x-mcp-header` are rejected at deploy; `$dynamicRef` and `$recursiveRef` pass deployment but every call fails. - YAML anchors, aliases, and merge keys (`<<`) cannot be used under `mcp`. - Even without `mcp:`, `/api/mcp` and paths beginning with `/api/mcp/` are reserved for MCP in apps that run server-side code (see [Reserved URL paths](https://keelson.dev/docs/reference/deploy-spec/#reserved-url-paths)). ### mcp.file When there are many tools, `instructions` and `tools` can live in a separate YAML file. `keelson.yaml` then contains only `enabled` and `file`. ```yaml command: "python app.py" mcp: enabled: true file: mcp.yaml ``` ```yaml # mcp.yaml (in the same folder as keelson.yaml) instructions: "Look up stock levels and record incoming deliveries." tools: - name: search_items description: "Search stock by item name." access: read input: type: object properties: query: type: string required: [query] ``` - At deploy time, the Keelson CLI inserts the separate file's contents into `mcp` in `keelson.yaml` before validating and uploading. Your local `keelson.yaml` is not rewritten. - Only `instructions` and `tools` may appear at the top level of the separate file (both optional). `mcp.file` cannot be combined with `mcp.instructions` or `mcp.tools`. - The path is relative to the folder containing `keelson.yaml` and cannot point outside it (including through `..` or symbolic links). The extension must be `.yaml` or `.yml`, and the file must be 4 MiB or smaller. - Errors inside the separate file are reported with the file name and location, such as `mcp.yaml: tools[3].input ...`. - Only a recent Keelson CLI reads `mcp.file`. Older CLIs reject the deployment with `mcp contains unsupported field(s): file`; run `keelson upgrade` to get the latest CLI. It cannot be used through paths that bypass the CLI. --- ## workers (not supported) `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. For work that runs in response to a request, declare it in `tasks` and enqueue it with the SDK ([Background tasks](https://keelson.dev/docs/building-apps/background-tasks/)). For a periodic drain of pending work, define a scheduled job in `crons`. Use an interval allowed by the plan; process pending items and exit on each run. ```yaml # Invalid: deployment is rejected workers: - name: drain command: "python worker.py" every: 10m # Valid: use crons crons: - name: drain schedule: "*/5 * * * *" command: "python drain.py" timeout: 120 ``` --- ## storage `storage` accepts only the deprecated `disk_id` field. The value is ignored; it does not enable persistent storage or automatic synchronization of `/data`. Remove this block if it remains in your configuration. | Field | Type | Required | Default | Description | |---|---|---|---|---| | `disk_id` | string | No | `null` | Deprecated and ignored identifier; 1–32 lowercase letters, digits, or hyphens | --- ## assets Configures static asset delivery for static sites, SPAs, and hybrid applications that combine static files with a backend API. ```yaml 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` | SPA fallback, such as `index.html`, relative to `dir`. Required for hybrid apps with `type: web` | | `api` | string | No | `null` (effective value `/api` in hybrid apps) | Path prefix forwarded to the backend. Defaults to `/api` when both `command` and `assets` are present | In hybrid apps, requests under `api` go to the backend; other requests serve static assets. The default prefix is `/api`; set this field only to use a different prefix. Rules: - `dir` cannot contain `..`. - `api` must begin with `/`, cannot be `/` itself, and cannot be under `/__keelson`. - `api` requires a top-level `command`. - A hybrid app with `type: web`, `assets`, and `command` requires `fallback`. If `type` is omitted, configuration validation allows `fallback` to be omitted. Set a fallback such as `index.html` when serving an SPA. ### Archive contents Static sites (`type: web` and `assets`, without `command`) upload only files under `assets.dir` and `keelson.yaml` by default. Container and hybrid apps also include source needed for the build. The exclusions below apply to both. Avoid `assets.dir: .` and equivalent paths such as `./`. They make the project root the public asset directory, potentially exposing every file in the archive. The CLI warns before deployment but does not reject this setting. #### Automatically excluded files 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 the file selected by `--secrets-from-env-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. #### Customizing exclusions with `.keelsonignore` Create `.keelsonignore` in the project root for project-specific exclusions. `.gitignore` is not read. | Syntax | Meaning | |---|---| | One pattern per line | Exclude matching files or directories; the last matching line wins | | A line beginning with `#` | Comment | | `/`, trailing `/`, `*`, `?`, `**`, `[abc]`, `[a-z]` | Gitignore-like path patterns | | Leading `!` | Re-include matching files or directories | Negation can deliberately re-include an env file such as `!.env.production`, but cannot override built-in exclusions such as `.git`, include `.keelsonignore` itself, or include the file selected by `--secrets-from-env-file`. 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. #### Checking before upload Run `keelson deploy --check --json` to inspect exclusions and files that may contain secrets. - Detected preview credentials or app tokens block upload. - If only webhook signing keys are detected, the CLI warns and continues. - Files whose names suggest sensitive content also produce warnings. Warnings do not exclude files; add exclusions to `.keelsonignore` as needed. `--check` also validates major settings locally. However, the API validates unknown keys inside `assets`, `crons`, `auth`, and `secrets`, cron expression validity, and `verify` path formats. A deployment can still fail with `deploy.config.invalid` after `--check` passes. #### Check output and scan coverage | JSON field | What it reports | |---|---| | `archive.excluded` | Excluded files | | `archive.excluded_env_files` | Excluded env files | | `archive.secret_like_files` | Files suspected of containing secrets based on their names or other indicators | | `archive.embedded_credentials` | Paths and types of credentials detected inside files; values are never shown | | `archive.unscanned_files` | Files whose contents were not scanned, for example because of size limits | | `archive.unscanned_binary_count` | Number of files classified as binary and not content-scanned | Files over 16 MiB, binary files, and files excluded from the archive are not content-scanned. `secret_like_files` includes names such as `credentials.json` and `*.pem`, plus existing files selected by an earlier `--secrets-from-env-file` deployment that are not excluded this time. This includes names such as `prod-secrets.txt` that do not follow env-file naming rules. To identify previously selected files, the CLI records their project-relative paths in `.keelson-config/previous-secrets-files.json`. The record contains no secret values and is automatically excluded from uploads. ### Browser caching 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. After redeploying a static site (`edge-static` / `edge-spa`), new files can take up to about 60 seconds to reach the edge (measured 1–57 seconds, median about 30). Wait 60 seconds before concluding a file is missing. --- ## health Sets the deployment health check path. Use a lightweight endpoint if `/` is expensive or depends on the database. ```yaml health: path: /health ``` - Defaults to `/`. - Must start with `/`, contain no `..`, not start with `/__keelson`, and be at most 2,048 characters. - HTTP 2xx, 3xx, and 404 pass; other 4xx and all 5xx fail. Because 404 passes, success proves the process started, not that the route exists. - The probe goes directly to the candidate revision's internal URL, bypassing the public host and authentication. Use [verify](#verify) to check public paths. --- ## secrets Declares the app's secrets and required combinations. Set values separately through the console, CLI, or an env file during deployment; do not put values here. ```yaml secrets: items: - name: OPENAI_API_KEY description: "OpenAI API key" - name: ANTHROPIC_API_KEY description: "Anthropic API key" required: - any_of: [OPENAI_API_KEY, ANTHROPIC_API_KEY] message: "Set at least one AI provider key" ``` ### Secret definitions | Field | Type | Required | Default | Description | |---|---|---|---|---| | `items` | list | No | `[]` | Secret definitions | | `items[].name` | string | Yes | — | Environment variable name: starts with an uppercase letter or `_`, contains only `A-Z0-9_`, and is at most 255 characters. `KEELSON_` names are reserved | | `items[].description` | string | No | `""` | Human-readable purpose of the secret | | `required` | list | No | `[]` | Rules checked against configured secrets | | `required[].any_of` | list | Conditional | `null` | Nonempty list of names; at least one must be set | | `required[].all_of` | list | Conditional | `null` | Nonempty list of names; all must be set | | `required[].message` | string | No | `""` | Guidance displayed when requirements are unmet | 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. Names are normalized to uppercase when values are saved, while `required` matching is case-sensitive. If you declare `name: api_key`, its value is saved as `API_KEY`, so the requirement does not match. Use uppercase names from the start. Names with hyphens, such as `MY-KEY`, can be declared but cannot have values set. For a new app, `keelson deploy --new --secrets-from-env-file ` creates the app, registers secrets, and starts its first deployment in one command. Changes to an existing app's secrets take effect on the next deployment. --- ## auth Configures application endpoints that are intended for external systems, such as webhook senders, and therefore do not use the normal interactive Keelson login flow. ```yaml auth: endpoints: - /api/webhooks/stripe - 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. Paths use **prefix matching**: `/api/external/status` also matches `/api/external/status-extra`. Methods are case-insensitive; omitting them allows every method. `methods` only applies to `/api/external/`, not `/api/webhooks/`. Restrict webhook methods in app code. Bypassing interactive login does not bypass Keelson authentication. Declaring an endpoint alone is insufficient; callers must supply Keelson-issued credentials. | Path | Required credentials | |---|---| | `/api/external/...` | App token with the `api` scope in `Authorization: Bearer` | | `/api/webhooks/...` | App token with the `webhook` scope in `X-Webhook-Secret`, or a URL registered with the external service in the form `/api/webhooks//...` | Issue tokens with `keelson apps tokens create`. See [External integrations](https://keelson.dev/docs/building-apps/external-integrations/) for URL construction. A standard Stripe webhook carrying only a Stripe signature, or a client carrying only an app-defined API key, is rejected before reaching the app. Requests to these paths carry `X-Keelson-User-Id` as an empty string (the header is always set, empty when there is no interactive user). Treat an empty value as "no user". In addition to Keelson credentials, verify webhook signatures or API keys in the app. --- ## verify Adds paths to deployment verification. By default, only `/` is checked. ```yaml verify: - /dashboard - /settings ``` Use this to check entry points that cannot be reached from `/`, such as pages accessible only through client-side routing. - A single path may be a string or a one-item list. - At most 20 paths are allowed after trimming whitespace and removing duplicates. Each must start with `/`, must not start with `//`, and must not contain `..` path components or control characters. - Container and hybrid deployments fetch paths from the candidate revision through the public host. Static sites and SPAs resolve paths against the release manifest before publication. - 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. --- ## region Optionally selects the placement region when creating a new application. ```yaml region: jp-tokyo ``` Use a Keelson logical region key, not a cloud-provider region identifier: | Key | Display name | New apps | |---|---|---| | `jp-tokyo` | Japan | Available (default) | | `us-oregon` | US West | Not yet offered (an explicit selection is rejected) | 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 modes 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. Static sites and SPAs also require `type: web`. | Name | Raw `deploy_mode` | `command` | `assets` | `fallback` | Description | |---|---|---|---|---|---| | Web application | `container` | Present | Absent | — | Ordinary application deployment | | Static site | `edge-static` | Absent | Present | Absent | Static files only | | SPA | `edge-spa` | Absent | Present | Present | Static files with a fallback | | Hybrid | `hybrid` | Present | Present | Required with `type: web` | Static files plus a backend API | --- ## Cross-field validation Some fields are valid only in specific combinations. | Rule | Requirement | |---|---| | A start command, scheduled jobs, or a static site is required | Supply `command`, `crons`, or both `type: web` and `assets` | | Tasks need code that enqueues them | When `tasks` is present, supply `command` or `crons` (`tasks_require_command_or_crons`) | | Task and job names are separate | A `tasks[].name` cannot match a `crons[].name` (`task_name_conflicts_with_cron`) | | Static sites cannot use scheduled jobs | Combining `type: web` and `crons` fails configuration validation. Omit `type` when using scheduled jobs | | Static sites have no container | `db.migrate` does not run for static sites or SPAs. Use a container or hybrid deployment for migrations | | `workers` is retired | Replace every `workers` declaration with `tasks` (work that runs after a request) or `crons` (time-triggered work) | | Managed migration requires managed data | `db.migrate` requires `db.mode: libsql` | | Local SQLite is disposable | `db.local_sqlite.paths` accepts only `/tmp/` paths and `:memory:` | | `mcp` requires an app that runs server-side code | `mcp` can be written only with `command` and without `assets` (`mcp is only supported for container apps`) | | `assets.api` requires a backend | Set a top-level `command` before using an API prefix | | Hybrid deployments with `type: web` require a fallback | Set `assets.fallback` when `type: web`, `assets`, and `command` are present. This check does not apply if `type` is omitted | --- ## Complete examples ### Web application with Managed SQLite ```yaml slug: crud-app description: "An internal CRM for managing customers and deals" runtime: python-slim command: "python app.py" db: mode: libsql migrate: "python migrate.py" env: PYTHONUNBUFFERED: "1" ``` ### Static SPA Run a local build such as `npm run build` to create `dist` before deployment. Keelson does not build a static site. ```yaml slug: marketing-site type: web runtime: node-slim db: mode: none assets: dir: dist fallback: index.html ``` ### Scheduled jobs only ```yaml slug: daily-report runtime: python-slim db: mode: libsql env: PYTHONUNBUFFERED: "1" crons: - name: generate schedule: "0 9 * * *" command: "python report.py" timeout: 120 ``` ### Web application with a background task The web application enqueues `generate-pdf` with the SDK when it receives a request. Keelson runs the task's command in an instance separate from the web application. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/). ```yaml slug: pdf-reports runtime: python-slim command: "python app.py" db: mode: libsql env: PYTHONUNBUFFERED: "1" tasks: - name: generate-pdf command: "python generate_pdf.py" timeout: 180 max_attempts: 3 ``` ### Web application with a scheduled drain To run work after a request without background tasks, store it in the database and process it with a scheduled job. Work waits until the next run. To start work right after a request, use [background tasks](https://keelson.dev/docs/building-apps/background-tasks/) instead. 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. ```yaml slug: task-drainer runtime: python-slim command: "python app.py" db: mode: libsql env: PYTHONUNBUFFERED: "1" crons: - name: drain schedule: "*/5 * * * *" command: "python drain.py" timeout: 120 ``` ### Hybrid static files and backend API ```yaml slug: photo-galleries runtime: node-slim command: "node server.js" db: mode: libsql env: NODE_ENV: "production" assets: dir: dist fallback: index.html api: /api ``` ### Go application ```yaml slug: my-go-app runtime: go-slim command: "./app" db: mode: none ``` Keelson builds the Go source for Linux and places the binary at `./app` during deployment. ### API receiving webhooks ```yaml slug: payment-hooks runtime: node-slim command: "npm start" db: mode: libsql env: NODE_ENV: "production" secrets: items: - name: STRIPE_WEBHOOK_SECRET description: "Stripe webhook signing secret" required: - all_of: [STRIPE_WEBHOOK_SECRET] auth: endpoints: - path: /api/webhooks/stripe methods: [POST] ``` Include an app token with the `webhook` scope in the URL registered with Stripe: `https:///api/webhooks//stripe`. `methods` does not apply to webhooks; reject methods other than POST in app code if needed. --- ## Common validation errors | Cause | Resolution | |---|---| | `db` or `db.mode` is missing | Declare `db.mode` as `libsql` or `none` | | The retired `databases` key is present | Remove it; use Managed SQLite or declare disposable local SQLite explicitly | | A local SQLite path points outside `/tmp/` | Use `/tmp/`, `:memory:`, or Managed SQLite for durable data | | `assets.api` does not start with `/` | Use an absolute path prefix such as `/api` | | No start command, scheduled jobs, or static site configuration | Add `command`, `crons`, or both `type: web` and `assets` | | `workers` is declared | Remove it; workers are rejected with `workers_not_supported`. Use `tasks` for work that runs after a request and `crons` for time-triggered work | | `crons[].timeout` is greater than 600 | Reduce the timeout to the platform ceiling or lower plan limit | | `tasks` is present without `command` or `crons` (`tasks_require_command_or_crons`) | Add the `command` of the web app that enqueues the tasks, or a job in `crons` | | A task has the same name as a scheduled job (`task_name_conflicts_with_cron`) | Rename the task or the job | | `tasks[].timeout` is greater than 600 (`TASK_TIMEOUT_EXCEEDS_STANDARD_LIMIT`) | Reduce it to 600 or less, and to the plan limit (`plan_task_timeout`), or split the work into smaller tasks | | `mcp.enabled` is `true` but `mcp.tools` is missing | Declare at least one tool, or move the tools into a separate file with `mcp.file` | | A static site, SPA, or hybrid app has `mcp` (`mcp is only supported for container apps`) | Remove `mcp`, or make the app a `command`-only app | | `mcp.tools[].input` contains `$ref` or `x-mcp-header` | Write the schema out in full. Do not use `$dynamicRef` or `$recursiveRef` either | | `mcp contains unsupported field(s): file` | The Keelson CLI is too old. Run `keelson upgrade` to get the latest version | | An `env` value is unquoted | Quote every value, including booleans and numbers | | An environment variable starts with `KEELSON_` | Rename it; that namespace is platform-owned | | A hybrid deployment with `type: web` omits `assets.fallback` | Set a fallback file such as `index.html` | | `command` installs dependencies (`command_installs_dependencies`) | 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 | | `runtime` and the language in `command` differ (`runtime_command_mismatch`) | Choose the matching runtime or fix the start command | | Build fails with `reserved_path_conflict` | Rename the `__keelson` directory at the root of the build output | --- # Environment variables Variables Keelson sets for your app URL: https://keelson.dev/docs/reference/environment-variables/ Language: en --- This page lists the environment variables Keelson sets in app containers and the variables used to run the [Keelson SDK](https://keelson.dev/docs/building-apps/sdk/) locally. Do not declare or override automatically supplied variables in `env` in `keelson.yaml`; use the values Keelson provides. Names beginning with `KEELSON_` are reserved and cannot be used in `env` or `secrets`. ## Core environment variables | Variable | Description | |---|---| | `PORT` | Port the app listens on. Read this value; do not set it | | `TZ` | Workspace time zone (for example, `Asia/Tokyo`) | | `KEELSON_MODE` | `keelson` on Keelson. Usually unset during local development; `local` also selects local storage for the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) and local synchronous runs for the [Tasks SDK](https://keelson.dev/docs/building-apps/background-tasks/#run-locally) | | `KEELSON_APP_ID` | Internal app 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 deploy | | `KEELSON_APP_URL` | App URL (`https://--.keelson.run`). Use when an absolute URL is required. Set when Keelson can generate the app's public hostname | | `KEELSON_DIRECTORY_BASE_URL` | Directory API base URL used by the [Identity SDK](https://keelson.dev/docs/building-apps/sdk/). Set to the same value as `KEELSON_APP_URL`, under the same condition | The former tenant-named variable remains accepted as a compatibility alias. No removal date is set. ## SDK and feature environment variables The [Files / Media / Identity / Tasks / Email SDKs](https://keelson.dev/docs/building-apps/sdk/) read their connection settings and credentials automatically. App code generally does not need to access these values directly. | Variable | Condition | Description | |---|---|---| | `KEELSON_DB_URL` | `db.mode: libsql` | Managed SQLite connection URL | | `KEELSON_DB_AUTH_TOKEN` | `db.mode: libsql` | Managed SQLite authentication token | | `TURSO_DATABASE_URL` / `TURSO_AUTH_TOKEN` | `db.mode: libsql` | Aliases for the two variables above (for compatibility) | | `KEELSON_FILES_BUCKET` / `KEELSON_FILES_PREFIX` | Container deployments with file storage configured (automatic for standard deployments) | Storage location used by the Files SDK | | `KEELSON_INTERNAL_MEDIA_BASE_URL` / `KEELSON_APP_MEDIA_TOKEN` | Container deployments with Media enabled (automatic for standard deployments) | Upload destination and authentication token used by the Media SDK | | `KEELSON_MEDIA_URL_PREFIX` | Same condition as the Media SDK upload destination and token | Media delivery URL prefix (`/__keelson/media/`) | | `KEELSON_DIRECTORY_TOKEN` | After `keelson apps directory enable` and redeployment | Directory API token used by the Identity SDK. Handle only on the server | | `KEELSON_EMAIL_API_URL` / `KEELSON_EMAIL_TOKEN` | Container deployments (automatic for standard deployments) | Endpoint and authentication token the [Email SDK](https://keelson.dev/docs/building-apps/external-integrations/#send-email) uses to send email. Handle only on the server | | `KEELSON_EMAIL_WEBHOOK_SECRET` | Same condition as `KEELSON_EMAIL_TOKEN` | Key for verifying the signature of [email delivery result](https://keelson.dev/docs/building-apps/external-integrations/#receive-email-delivery-results) notifications. Pass it to the Email SDK's verification function | | `KEELSON_TASKS_BASE_URL` | Set in web, scheduled job, and task containers while Keelson accepts background tasks (whether or not the app declares `tasks`) | URL the [Tasks SDK](https://keelson.dev/docs/building-apps/background-tasks/) uses to enqueue tasks and read their status. You do not set it yourself | Static sites and SPAs (`assets` without `command`) have no container, so no runtime environment variables are set. They cannot use the Files or Media SDKs. Apps that run only scheduled jobs also omit `command`, but their jobs run in containers and can use SDK environment variables. ## Not set - **`NODE_ENV`** — Not set automatically. Declare `"production"` in `env` in `keelson.yaml` - **External database connection details** — Pass them as secrets. With `db.mode: none`, Managed SQLite connection details are not set automatically ## Used for local development Set these in your local environment as needed to run the SDKs during development. Do not register them in `env` in `keelson.yaml` or as Keelson secrets. | Variable | Description | |---|---| | `KEELSON_MODE=local` | Use local storage with the Files / Media SDKs. With the Tasks SDK, `enqueue` runs the command synchronously on your machine through the CLI's `keelson dev task run`. Both also work this way when this is unset in a standard local development environment | | `KEELSON_FILES_DIR` | Local storage location for the Files SDK (default: `./.keelson/files`) | | `MEDIA_DIR` | Local storage location for the Media SDK (default: `./media`) | | `KEELSON_LOCAL_MODE=1` | Make the Identity SDK return test data without calling the API. Not needed when you start through `keelson dev serve` ([Run your app locally](https://keelson.dev/docs/building-apps/local-development/)) | | `KEELSON_LOCAL_USERS_FILE` | User list file the Identity SDK reads when `KEELSON_LOCAL_MODE=1` (default: `./.keelson/dev-users.json`) | | `KEELSON_LOCAL_USER_ID` / `KEELSON_LOCAL_USER_EMAIL` / `KEELSON_LOCAL_USER_NAME` | ID, email address, and display name of the test user returned by the Identity SDK. Not used when a user list file exists | | `KEELSON_LOCAL_WORKSPACE_ID` / `KEELSON_LOCAL_WORKSPACE_ROLE` / `KEELSON_LOCAL_APP_ID` | Test workspace ID, workspace role, and app ID returned by the Identity SDK | The former `KEELSON_LOCAL_TENANT_ID` and `KEELSON_LOCAL_TENANT_ROLE` names remain accepted as compatibility aliases. If both old and new names are set, the names containing `WORKSPACE` take precedence. No removal date is set. ## Related pages - [Environment variables and secrets](https://keelson.dev/docs/deploy/environment-variables/) — Values you configure - [Files and media](https://keelson.dev/docs/building-apps/files-and-media/) - [Using identity in your app](https://keelson.dev/docs/building-apps/using-identity/) --- # CLI commands keelson command reference URL: https://keelson.dev/docs/reference/cli/ Language: en --- This page lists the main `keelson` CLI commands and the behavior scripts and agents need to account for. An AI agent typically runs them through an Agent Skill, so you do not need to memorize them all. Use `keelson --help` for command-specific help. See the [Quickstart](https://keelson.dev/docs/introduction/quickstart/) to install the CLI. Run `keelson upgrade` to update it. In the examples below, `<...>` is a value to supply and `[...]` marks optional arguments or flags. Use durations such as `30s` or `5m` for `` and integers for ``. ## Common flags These flags are shared by several commands, but support varies. Commands such as `login`, `logout`, `install-agent`, and `dev ...` accept only some of them. `app curl` does not accept `--json` or `--quiet`. | Flag | Description | |---|---| | `--app ` | Target app; resolved from `keelson.yaml` in the current directory when omitted. Required for `access show`, `access set`, `apps tokens create`, `apps tokens rotate`, and `feedback` | | `--workspace ` | Target workspace by slug, name, or ID. Use the slug or ID if names are ambiguous | | `--json` | Output the result as JSON. Use this from scripts and agents | | `--quiet` | Suppress explanations and progress; output depends on the command | | `--timeout ` | API call timeout; default 15 seconds, with exceptions below | | `--retry ` | Retry count for temporary failures; default 0, with exceptions below | | `--no-browser` | Show a login or approval URL without opening a browser. Supported by `login`, `apps delete`, `db apply`, and `snapshots restore` | Workspace selection follows this order: `--workspace`, `workspace` in `keelson.yaml`, then the saved default workspace. If no workspace is selected, the CLI automatically selects your only workspace; if you belong to several, supply `--workspace`. The exception is `apps list`, which lists all workspaces when the flag is omitted. With `--quiet`, `secrets set` prints one line per secret name and `deploys list` prints one line per deployment ID. A successful `deploy --check` prints nothing. Timeout and retry exceptions: - `apps duplicate` defaults to a 120-second timeout. - The app request made by `app curl` defaults to a 130-second timeout. Temporary GET / HEAD failures are retried up to twice; use `--retry 0` to disable retries. - App creation in `deploy --new` retries temporary failures up to three times even with `--retry 0`. This retry count cannot be set below three. A value above three sets the higher limit. `preview` and `app curl` do not accept `--no-browser`, even when browser approval is required. In non-interactive execution, they print an approval URL without opening a browser. The same applies to `preview --json`. When a command fails with `--json`, standard output contains an `error` object. Its `code` identifies the error, `message` explains it, and `hint` describes what to do. If `retryable` is `false`, do not retry automatically; follow `hint`. Older names such as `--tenant` and `keelson tenants list` are still accepted. See [Command aliases](#command-aliases). ## Setup | Command | Description | |---|---| | `keelson login [--no-browser]` | Log in through a browser and link this device | | `keelson logout [--all-devices]` | Delete saved login credentials on this device and invalidate the server-side session. `--all-devices` logs out every device. Tokens supplied through `KEELSON_TOKEN` are not revoked | | `keelson whoami` | Show the signed-in user and currently selected workspace. Use `workspaces list --json` to reliably obtain a workspace slug | | `keelson workspaces list [--query ] [--exact]` | List or search your workspaces. Pass the `slug` from `--json` output to `--workspace` | | `keelson doctor [--fix-path]` | Check PATH, login, and Skill status. `--fix-path` adds PATH to the shell configuration file | | `keelson install-agent [--global]` | Install the Skill for an AI agent. With no argument and `--yes`, update all installed Skills | | `keelson upgrade` | Update the CLI to the latest version | | `keelson version` | Show the version | | `keelson telemetry status\|off\|on` | Show, disable, or enable [CLI usage telemetry](https://keelson.dev/docs/reference/telemetry/) | | `keelson feedback --app [--deploy ] < feedback.json` | Send feedback about the CLI and the Skill as JSON on standard input. See [Feedback](#feedback) | As an alternative to `keelson install-agent`, install the Skill from the public repository using the Claude Code plugin commands `/plugin marketplace add keelsonhq/skills` followed by `/plugin install keelson@keelson-skills`, or use `npx skills add keelsonhq/skills` for other agents. Use only one installation method; combining them can create two copies of the Skill or cause one installation to overwrite the other. These alternatives install only the Skill, so you must also [install the CLI and sign in](https://keelson.dev/docs/introduction/quickstart/). A Skill installed as a Claude Code plugin is not updated by `keelson login`; it follows Claude Code plugin updates instead. The plugin is outside the location checked by `keelson doctor`, so you can ignore its “not configured” result and its suggestion to run `install-agent` for Claude Code. ## Deploy | Command | Description | |---|---| | `keelson deploy` | Deploy the app in the current directory. See below for when it waits for completion | | `keelson deploy --new [--region ] [--secrets-from-env-file ]` | Create an app and deploy it for the first time. Register env file values as secrets and exclude that file from the archive | | `keelson deploy --check [--json]` | Validate configuration and show archive contents, exclusions, and potentially sensitive files without uploading. This local check cannot determine whether you will exceed plan limits | | `keelson deploy --ndjson --yes` | Stream progress as one JSON object per line for scripts | | `keelson status [deploy_id] [--wait] [--max-wait ]` | Show the specified deployment's status, or the app's latest deployment if no ID is supplied | | `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 [--limit ] [--offset ]` | Show deployment history | | `keelson deploys download [-o ]` | Download the source archive used for a deployment, within its retention period | | `keelson rollback [deploy_id] [--yes]` | Return to the previous successful deployment or a specified successful deployment | ### When deploy waits for completion | Invocation | Behavior | |---|---| | `keelson deploy` in an interactive terminal | Waits for completion; default 30 minutes, configurable with `--watch-timeout` | | `keelson deploy --yes` in a pipe, CI, or other non-interactive environment | Returns when accepted. Use `--yes --watch` to wait | | `--json` | Returns when accepted; cannot be combined with `--watch` | | `--ndjson` | Waits for completion; cannot be combined with `--no-watch` | | `--no-watch` | Returns when accepted, even in an interactive terminal | In normal output mode, non-interactive execution requires `--yes`; omitting it produces `confirmation_input_unavailable`. `--json`, `--ndjson`, and `--quiet` run without asking for confirmation. Reaching the watch timeout stops monitoring, but does not stop the server-side deployment. Continue checking with `status --wait`. ### Check completion with status `status --wait` waits up to 50 seconds by default. Set a different limit with `--max-wait `. At the limit it returns the latest status it retrieved. **If status retrieval succeeds, the exit code is `0` even when the deployment is still running.** Scripts must inspect `terminal` and `status` in `--json` output, rather than relying on the exit code alone. `terminal: false` means the deployment is unfinished. Success requires `terminal: true` and `status: "completed"`. A previous successful revision may still be serving even when the latest deployment failed. Use `keelson app info` to check the app currently serving traffic. ### Rollback behavior Only successful deployments with a container image can be rollback targets. To revert a static site, redeploy its previous source. Rollback skips the build but runs `db.migrate` again. `rollback` returns when accepted. Pass the returned deployment ID to `keelson status --wait --json` and inspect `terminal` and `status`. `--yes` is required with `--json` or non-interactive execution. ### Reading NDJSON NDJSON contains progress lines with fields such as `stage` and `status`. During a health check, for example, `stage` is `health_check` and `status` is `progress`. A progress line's `status` does not determine the outcome of the whole deployment. - If monitoring starts, a final line contains `result`, with the value `success` or `failed`. - If the command fails before monitoring starts, it emits an `error` line without a `result` line. Invalid configuration may produce only an error line. Failures after registering secrets or uploading the archive may also have earlier progress lines. - Check the process exit code too. A nonzero code means the CLI operation failed. A monitoring timeout also counts as failure, although deployment may still be running on the server. ## Verify | Command | Description | |---|---| | `keelson app curl [-i] [--method ] [--data ] [--form ] [--header ]` | Send an authenticated request to the app. See [Verify a deploy](https://keelson.dev/docs/deploy/verify-deploy/) | | `keelson preview [--ttl ] [--allow-writes] [--json]` | Issue an expiring token for authenticated access to the app | | `keelson app info` / `start` / `stop` / `restart` | Show app information, start, suspend, or restart the app. `restart` waits for completion; use `--no-watch` to return when accepted | With `app curl --data`, also specify `--method POST`, `PUT`, `PATCH`, or `DELETE`. `--form` defaults to POST when `--method` is omitted. Repeat `--form` or `--header` for multiple fields or headers. The response body is written to standard output. `-i` also writes the HTTP status and response headers to standard error. Temporary GET / HEAD failures are retried up to twice by default; use `--retry 0` to disable retries. If someone other than the person who deployed the currently serving revision runs `preview` or `app curl`, browser approval is required even for reads. Approval alone does not issue the token or send the request. Within 30 minutes of approval, rerun the same command with `--confirmation `. This differs from the approval flow in [Operations that require browser approval](#operations-that-require-browser-approval). ## Logs | Command | Description | |---|---| | `keelson logs app [--mode live\|history] [--severity ] [--previous] [--limit ] [--since ]` | (`--previous` is not supported on the current runtime and returns 422; crashed-instance output is included in the normal stream) Show the app's standard output and standard error | | `keelson logs cron [--severity ] [--limit ] [--since ]` | Show scheduled job execution logs | | `keelson logs access [--limit ] [--since ]` | Show access logs | | `keelson logs deploy [deploy_id] [--limit ] [--since ]` | Show deployment progress and stored failure details, including startup logs when the app exited during startup. Successful build output is not retained; the command explains when no logs are available | `--since` accepts durations such as `1h` or `1d12h`, from 1 second through 30 days. `--json` returns one JSON object containing log arrays and other fields. The log location depends on the command. | Command | Logs in JSON | |---|---| | `logs app` / `logs cron` | `entries`: objects containing the timestamp, `severity`, message, and other fields | | `logs access` | `entries`: objects containing the timestamp, HTTP status, path, and other fields | | `logs deploy` | `logs`: progress log objects. `failure_logs` and `startup_logs`: arrays of strings | ## Secrets Pass values through standard input or an env file, not as command arguments. | Command | Description | |---|---| | `keelson secrets list` | Show configured names and update times, without values | | `echo -n "" \| keelson secrets set [--apply]` | Set one value. `--apply` also requests redeployment and returns when accepted | | `keelson secrets set --from-env-file [--apply]` | Set multiple values from an env file. Keys are saved individually; a failure partway through leaves a partial update. The error lists saved keys | | `keelson secrets unset ` | Delete a secret | Secrets take effect on the next deploy, or when you use `--apply`. ## App management | Command | Description | |---|---| | `keelson apps list [--workspace ] [--query ] [--exact]` | List apps. Omitting `--workspace` includes all your workspaces. JSON results use an `apps` array when the flag is supplied and a `workspaces` array when omitted | | `keelson apps duplicate --name [--copy-secrets] [--enable-schedule]` | Duplicate an app; scheduled jobs are disabled by default | | `keelson apps rename ` | Change the slug in the app URL. **The old URL returns 410 without an automatic redirect.** The change takes time to propagate | | `keelson apps delete [slug]` | Delete an app. **Browser approval is required** | | `keelson quotas` | Show plan limits and usage | After `apps rename`, update `slug` in your local `keelson.yaml` too. ### What a duplicate carries over A duplicate is created from the source app's latest successful deploy. The duplicate is a separate app with its own URL and deploy history. | Item | What happens | |---|---| | Application code | Copied from the latest successful deploy | | Configuration (`keelson.yaml`) | Copied, with the duplicate's new app identity | | Scheduled job definitions | Copied, but created disabled after the duplicate's deploy succeeds. `--enable-schedule` enables jobs unless their configuration explicitly sets `enabled: false` | | App-level secrets | Not copied by default. Use `--copy-secrets` to copy them | | Workspace-level secrets | Not copied, but they remain available because workspace-level secrets are provided to every app in the workspace | | Platform-managed credentials | New credentials are issued for the duplicate | | Managed database contents | Not copied; the duplicate gets a new, empty database. The CLI has no option to copy database contents | | Deploy history | Not copied; the duplicate starts with a new deploy based on the source app's latest successful deploy | ## Scheduled Jobs | Command | Description | |---|---| | `keelson crons list [--include-disabled]` | List jobs | | `keelson crons trigger [--wait]` | Run a job now; `--wait` waits for its result | | `keelson crons enable ` / `disable ` | Enable or disable a job | | `keelson crons runs list [--cron ] [--limit ] [--offset ]` | Show execution history | | `keelson dev cron run [--file ] [--dir ]` | Run a scheduled job from `keelson.yaml` once locally | | `keelson dev task run [--payload \| --payload -] [--file ] [--dir ] [--json]` | Run a background task from `keelson.yaml` once locally, synchronously, with no retry. `--payload` is the task's `payload` as JSON (`-` reads it from standard input; omitted means `null`). With `--json`, standard output carries one result object. The SDK uses this command when `enqueue` runs locally. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/#run-locally) | ## Local development | Command | Description | |---|---| | `keelson dev serve [--port ] [--users ] [--as ] -- ` | Start your app's server with `` and relay requests to it, adding the same user information headers as Keelson. Switch users with the switch page (`/__keelson/dev`) or the `X-Keelson-Dev-As` header. `--port` is the port to listen on (default `5173`), `--users` is the user list file (default `./.keelson/dev-users.json`, or four built-in users if absent), and `--as` is the user at startup. The app receives a free port in `PORT`, and `{port}` in `` is replaced with it. Does not start in a deployed environment. See [Run your app locally](https://keelson.dev/docs/building-apps/local-development/) | | `keelson dev serve --check` | Exit with code 0 without starting anything. Use it to check whether this CLI supports `dev serve` | ## Database | Command | Description | |---|---| | `keelson db apply [file.sql]` | Run SQL against Managed SQLite; omit the file or use `-` for standard input. **Browser approval is required if the DB already contains schema objects.** A first-time DB with no tables, indexes, views, or triggers needs no approval | | `keelson snapshots list` | List restore points and running or recent exports, including successful and failed exports | | `keelson snapshots create` | Create a manual snapshot | | `keelson snapshots download [-o ]` | Download a backup. Without `-o`, only prints a download URL and writes no file | | `keelson snapshots export --at ` | Create a point-in-time export (PITR), then retrieve it with `download` after completion | | `keelson snapshots restore ` | Restore a snapshot. **Browser approval is required**. The restore can be undone within 72 hours | | `keelson snapshots restore-status` | Check the progress and result of the latest restore | | `keelson db revoke-tokens [app] [--yes]` | Revoke DB authentication tokens (destructive). Prompts in the terminal; non-interactive use requires `--yes`. The DB is temporarily unavailable until the new revision takes over | | `keelson db recover [app]` / `credential-status [app]` | Recover credentials and show their status | ## Access control | Command | Description | |---|---| | `keelson groups list` | List groups | | `keelson groups create [--name ] [--description ] [--if-not-exists]` | Create a custom group | | `keelson groups members list ` | List group members | | `keelson groups members add\|remove ...` | Change group membership for existing members; does not send invitations | | `keelson access show --app ` | Show the app's view and manage groups | | `keelson access set --app [--view ]... [--manage ]... [--allow-self-lockout]` | Set groups with view and manage permissions | `access set` replaces all bindings for each permission type you specify. Supplying only `--view` leaves manage permissions unchanged. Use `--view none` to clear view groups. Manage groups cannot be empty. Repeat the flag for multiple groups. For example, to replace view groups with `sales` and `engineering`: ```sh keelson access set --app my-app --view sales --view engineering ``` A change that removes your own manage permission requires `--allow-self-lockout`. ## External integrations | Command | Description | |---|---| | `keelson apps tokens create --app --name --scope [--scope ]... [--allowed-ip ]...` | Issue an app token, shown only once. `--app` is required | | `keelson apps tokens list` | List token metadata only | | `keelson apps tokens rotate --app --token-id ` | Rotate a token; the old token expires after a grace period | | `keelson apps tokens delete ` | Revoke a token | | `keelson apps directory enable` | Issue a Directory API token and register it as `KEELSON_DIRECTORY_TOKEN` (redeploy required) | | `keelson domains list` | List custom domains and their status | | `keelson domains add\|verify\|remove [--yes]` | Add, verify, or remove a custom domain (Team). Non-interactive `remove` requires `--yes` | Allowed `--scope` values are listed below. Inbound and Directory scopes cannot be combined in one token. | Scope | Purpose | |---|---| | `api` | Call the app's `/api/external/...` endpoints from external systems | | `webhook` | Receive webhooks from external services | | `directory:members:read` / `directory:identity:read` / `directory:groups:read` / `directory:group-members:read` | Call the Directory API from the app; normally granted by `apps directory enable` | ## Feedback `keelson feedback --app [--deploy ] < feedback.json` sends feedback about the CLI and the Skill. The payload is one JSON object on standard input. Unknown fields are rejected. | Field | Description | |---|---| | `verdict` (required) | Object with required `score` (integer 1–10) and `summary` (1–1,000 characters) | | `friction` | Up to 20 objects with `step`, `command`, `output_quote`, `expected`, `lost_minutes`, and `recovered` | | `yaml_confusions` / `error_message_reviews` / `wanted_commands` / `kept_good` | Up to 20 strings each | | `skill_gaps` / `verification_method` | Strings | Each `friction` entry requires all six fields: | Field | Type and meaning | |---|---| | `step` | String describing the task stage or situation where friction occurred | | `command` | String containing the command run | | `output_quote` | String quoting the message displayed | | `expected` | String describing the expected behavior | | `lost_minutes` | Integer from 0 to 1,440: minutes spent resolving the problem | | `recovered` | Boolean: `true` if resolved, `false` if unresolved | For example, save the following as `feedback.json` and submit it. Replace the content with your actual experience. ```json { "verdict": { "score": 8, "summary": "Deployment succeeded, but confirmation in non-interactive execution was unclear." }, "friction": [ { "step": "Deploying from CI", "command": "keelson deploy", "output_quote": "The confirmation response could not be read from standard input.", "expected": "I wanted to know the required flags for non-interactive execution in advance.", "lost_minutes": 3, "recovered": true } ] } ``` ## Command aliases These older names still work and no removal date is set. Prefer the canonical form; the alias is not shown in `--help` output as a separate command. | Alias | Canonical form | |---|---| | `keelson backups list` / `trigger` / `download` / `restore` | `keelson snapshots list` / `create` / `download` / `restore` | | `keelson tokens list` / `delete` | `keelson apps tokens list` / `delete` | | `keelson skill install [--global] [--codex] [--yes]` | `keelson install-agent [--global] [--yes]` | | `keelson tenants list` | `keelson workspaces list` | | `keelson db rotate-credentials` | `keelson db revoke-tokens` | | `--tenant` | `--workspace` | ## Operations that require browser approval Deleting an app, running SQL against a database with existing schema objects, and restoring a snapshot require browser approval. The command displays an approval URL (`confirmation_required`). Approving in the browser executes the operation on the server; do not rerun the command. Approval expires after 15 minutes. Behavior after the URL is displayed depends on execution mode: - Interactive terminal: waits for approval and execution by default. Use `--wait=false` to return without waiting. - `--json` or a non-interactive terminal: returns without waiting by default. Use `--wait` to wait. `preview` and `app curl` use the separate rerun flow described under [Verify](#verify). `rollback`, `db revoke-tokens`, and `domains remove` require terminal confirmation rather than browser approval; non-interactive execution needs `--yes`. ## Environment variables | Variable | Description | |---|---| | `KEELSON_TOKEN` | Token used instead of login, for example in CI. Takes precedence over saved credentials. Setting it to an empty string causes an error | | `KEELSON_NO_UPDATE_CHECK=1` / `KEELSON_NO_SKILL_SYNC=1` | Disable update checks / automatic Skill updates. Any nonempty `KEELSON_NO_SKILL_SYNC` value, including `0`, disables sync | | `KEELSON_NO_TELEMETRY=1` | Disable [CLI usage telemetry](https://keelson.dev/docs/reference/telemetry/) | | `KEELSON_CONFIG_DIR` | Directory containing `credentials.json` and `config.json` | | `KEELSON_API_BASE_URL` / `APP_DOMAIN_BASE` | Override the API endpoint and app domain; normally unnecessary | --- # Error codes and resolutions Errors in the console and CLI, and how to resolve deployment and app access failures URL: https://keelson.dev/docs/reference/error-codes/ Language: en --- This page covers common Keelson errors, where to find them, and how to resolve them. It includes CLI errors, deployment failures shown in the console, and errors when accessing deployed apps. For CLI errors, inspect `code`, `message`, and, when present, `hint`. For deployment failures in the CLI or API, inspect `failure_code` and `failure_hint`. Follow any guidance returned with the error before this page: it is specific to that situation. Include these details when asking an AI agent to investigate or fix a problem. This is not an exhaustive list. For unlisted codes, read the returned message and guidance. ## Where to find errors The location and response format depend on where the operation runs. | Location | How to inspect the error | |---|---| | Console | Open the app overview or deployment history to see the failure reason and logs | | CLI | Read the terminal error. With `--json`, standard output contains `error` with fields such as `code`, `message`, `hint`, and `retryable` | | Deployment failure (`keelson status` / `keelson diagnose`) | Inspect `failure_code` and `failure_hint` | | Keelson API | Messages are generally in `detail`; input validation can return an array. Some approval, snapshot, validation, and manage-permission errors use `error` with `code`, `message`, `hint`, and other fields | | App access denied at the gateway | Browsers receive an explanation page; fetch / XHR receives JSON such as `{"detail":"ip-restricted"}`. Check `X-Keelson-Auth-Error` for the reason | | No available app slot or suspended app at the gateway | HTTP 503 with `x-keelson-platform-error: capacity-full` or `app-suspended`. Capacity errors also include `X-Keelson-Error-Code: CAPACITY_FULL` | | Missing URL, suspended app, or reserved path at the edge | HTTP 404 / 410 / 503 / 403. Browsers receive an explanation page and API clients receive plain text; some reserved paths also return plain text to browsers. Inspect `x-keelson-platform-error`. These requests do not reach the gateway and have no `X-Keelson-Auth-Error` | | Files / Media delivery | Inspect `x-keelson-error`. Some codes also return JSON containing `error`, `hint`, and `doc` | Example CLI error with `--json`: ```json { "error": { "code": "not_logged_in", "message": "Not logged in.", "hint": "Run `keelson login` or set KEELSON_TOKEN, then retry the command.", "retryable": false } } ``` `retryable` indicates whether retrying the same operation may succeed. HTTP status alone is insufficient: a 503 may have `retryable: false`, and some build-related 429 errors, such as the cumulative trial allowance, do not clear by waiting. ## Pre-deploy checks (CLI) 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. A dependency manifest declares the libraries an app uses, for example `go.mod`, `requirements.txt`, or `package.json`. Go requires `go.mod` even without external libraries. | 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` conflicts with database connection code; rules cover a production connection that does not read `KEELSON_DB_URL`, a local-development `file:` fallback left on the production path, and declaration mismatches. Keeping `file:local.db` as the fallback when the environment variable is absent is fine | Follow `hint`. A common fix is to use a libSQL client that reads `KEELSON_DB_URL` | | `file_wiring_fail` (warning) | State files or uploads are written to local disk | Use the [Files / Media SDK](https://keelson.dev/docs/building-apps/sdk/) | | `icon.path` | The `icon` file is missing, unreadable, a symbolic link, or excluded from the upload | Put the image inside the project where it is uploaded, and set `icon` relative to `keelson.yaml`. See [icon](https://keelson.dev/docs/reference/keelson-yaml-reference/#icon) | | `icon.format` / `icon.size` / `icon.dimensions` / `icon.corrupt` | The `icon` image is not PNG, JPEG, or WebP; is larger than 256 KB; is outside 32 to 2048 pixels on a side; or has an unreadable header | Export the image again as PNG, JPEG, or WebP within the limits | ## keelson.yaml and archive validation The following errors prevent deployment. | Code | Cause | Resolution | |---|---|---| | `invalid_keelson_config` | 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; static sites also require `type` and `assets` | Add the missing field | | `env_value_not_string` | Non-string `env` values, ambiguous YAML keys, duplicate keys, or merge keys (`<<`) | Quote values or use block scalars. Quote ambiguous keys such as `on`, `yes`, or numbers. Remove duplicates and expand merge keys; quoting does not make duplicates or merge keys valid | | `description_too_long` | `description` is longer than 300 characters | Shorten it | | `workers_not_supported` | `workers:` is present | Remove it. Use `tasks` for work that runs in response to a request and `crons` for time-triggered work | | `DB_DATABASES_REMOVED` | `databases:` is present | Remove it. Use `db.mode: libsql` for persistent data | | `SQLITE_LOCAL_SQLITE_INVALID_PATH` | A `db.local_sqlite.paths` value is outside `/tmp/` | Use `/tmp/` or `:memory:` | | `CRON_TIMEOUT_EXCEEDS_STANDARD_LIMIT` | A `crons[].timeout` value exceeds 600 seconds | Reduce it to 600 seconds or less, or split the operation | | `plan_cron_limit` / `plan_cron_min_interval` / `plan_schedule_timeout` | Job count, interval, or timeout violates plan limits | Reduce the count, increase the interval, shorten the timeout, or choose a plan that supports the required settings | | `tasks_require_command_or_crons` | `tasks` is declared, but there is neither `command` nor `crons`, so no code can enqueue the tasks | Add the `command` of the web app that enqueues the tasks, or a job in `crons` | | `task_name_conflicts_with_cron` | A `tasks[].name` matches a `crons[].name` after removing surrounding whitespace and lowercasing | Rename the task or the job | | `TASK_TIMEOUT_EXCEEDS_STANDARD_LIMIT` | A `tasks[].timeout` value exceeds 600 seconds | Reduce it to 600 seconds or less, or split the work into smaller tasks | | `plan_task_timeout` | An explicit `tasks[].timeout` exceeds the plan's limit per run | Shorten the timeout, or choose a plan with a higher limit. An omitted `timeout` is reduced to the plan limit instead of being rejected | | `ARTIFACT_SQLITE_FILE_DRIVER` | A file-based SQLite client such as `sqlite3` or `better-sqlite3` was detected | Migrate to a libSQL client. For temporary use, declare it in `db.local_sqlite` | A `__keelson` directory at the root of the build output fails the build. The `failure_code` is `deploy.build.failed`, and the build log contains `reserved_path_conflict`. Rename the directory. ### Archive warnings These are warnings and do not fail deployment by themselves. Respond according to the purpose of the code. | Code | Cause | Resolution | |---|---|---| | `ARTIFACT_EXEC_NODE_CRON` / `ARTIFACT_EXEC_APSCHEDULER` | Scheduled work is registered inside the app process | It will not run while the app sleeps. Extract a standalone command and declare it in `crons` in `keelson.yaml` | | `ARTIFACT_EXEC_BACKGROUND_TASKS` | Work runs after the response is returned | `await` short external calls and work required for request success before returning the response. Declare work that continues after the response in `tasks` and enqueue it with the SDK ([Background tasks](https://keelson.dev/docs/building-apps/background-tasks/)) | | `ARTIFACT_EXEC_SET_INTERVAL` | Server-side code uses `setInterval` | No change is needed for work such as cache refreshes where missed runs are harmless. Move work requiring ongoing scheduled execution to `crons` | ## Deploy failures Check the app overview or deployment history in the console for the reason and logs. In `keelson status`, `keelson diagnose`, or the API, inspect `failure_code` and `failure_hint`. The table also includes errors that prevent deployment from being accepted. | Code | Cause | Resolution | |---|---|---| | `deploy.config.invalid` | Invalid `keelson.yaml` content | Correct the reported key and deploy again | | `deploy.config.secret_missing` | A secret declared in `secrets.required` is not set | Set every secret with `keelson secrets set ` (the value is read from standard input), then deploy again | | `deploy.artifact.invalid` | Uploaded archive failed validation, for example because a required lockfile is missing | Fix the files or code identified in the error | | `deploy.artifact.assets_invalid` | Files declared by `assets.dir` or `assets.fallback` are missing from the upload | Build locally and redeploy | | `deploy.build.failed` | Dependency resolution, a build script, or a reserved-path collision caused a build failure | Read the end of the build output with `keelson logs deploy ` | | `deploy.build.timed_out` | Build exceeded its time limit | Remove unnecessary dependencies or move expensive preprocessing out of the build | | `deploy.build.image_too_large` | The image is too large | Remove unnecessary dependencies or files | | `deploy.runtime.start_failed` | The app exited during startup or the container did not reach the running state | Read the stored startup logs with `keelson logs deploy `, then verify `PORT` and `0.0.0.0` | | `deploy.runtime.health_check_failed` | `/` or `health.path` did not return an accepted response (2xx / 3xx / 404) within the startup and health verification time limit. This includes exiting during startup or repeatedly returning 401 / 403 / 5xx | Read stored startup logs with `keelson logs deploy `; check the listening port and `health.path` | | `deploy.runtime.migration_failed` | `db.migrate` exited nonzero; the previous revision continues serving | Fix the migration and redeploy. Partially applied database changes are not automatically undone | | `deploy.verify.failed` | A `verify` path is unreachable, or JavaScript / CSS Content-Type does not match | Check paths and build output. Details appear as `verification_*` codes in `failure_logs` | | `deploy.plan.limit_exceeded` | A plan limit was exceeded | Identify the limit in `failure_hint`, adjust settings or usage, or choose a plan with the required limit | | `deploy.plan.cron_count_exceeded` | The app declares more crons than the plan allows | Reduce the number of crons or upgrade the plan | | `deploy.plan.cron_interval_too_short` | A cron fires more often than the plan's minimum interval | Increase the interval or upgrade the plan | | `deploy.plan.schedule_timeout_exceeded` | An explicit `crons[].timeout` exceeds the plan's ceiling | Lower the timeout or upgrade the plan | | `deploy.app.suspended` | Keelson has restricted the app or workspace | Contact support. A user or administrator cannot remove this restriction by resuming the app | | `deploy.app.operation_in_progress` | The deploy could not start because another operation was in progress for this app | Wait for the other operation to finish, then deploy again | | `deploy.platform.temporarily_unavailable` | Temporary outage | Wait and retry up to three additional times. If it still fails, contact support with `deploy_id` | | `deploy.platform.error` | Platform error | Retrying will not resolve it. Contact support with `deploy_id` | | `deploy.cancelled` | Deployment was cancelled | Redeploy if needed | | `deploy_in_progress` (409) | Another deploy or app operation is in progress | Wait for the operation to finish, then run the same command again | | `build_rate_limit_exceeded` / `build_guardrail_exceeded` (429) | 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 | ## Scheduled job failures and skipped runs Inspect `executions[].failure_code` in `keelson crons runs list --json`. Use `keelson logs cron` to investigate execution logs too. `cron.failed.*` means a job started and failed; `dispatch_failed` means it could not be started. `cron.skipped.*` means a scheduled run was skipped because its execution conditions were not met. | Code | Meaning | |---|---| | `cron.failed.command_failed` | Command exited nonzero | | `cron.failed.timed_out` | Exceeded `timeout` | | `cron.failed.terminated_no_callback` | Execution ended without reporting a result | | `dispatch_failed` | Could not start execution | | `cron.skipped.schedule_disabled` | Job is disabled | | `cron.skipped.quota_exceeded` | Plan execution allowance exceeded | | `cron.skipped.due_to_lock` / `cron.skipped.lease_held` | Previous run has not finished | | `cron.skipped.missed_run` | Skipped because the scheduled time had passed | | `cron.skipped.app_suspended` | App is suspended | | `cron.skipped.plan_downgrade` | A plan change made the job ineligible to run | ## Background task errors These errors are returned by the Tasks SDK when the app calls `enqueue` or `get`. Each SDK raises or returns one error type (Python and Node.js `TasksError`, Go `*tasks.Error`) with `code`, `status`, and `message`. Branch on `code`; do not depend on the wording of `message`. See [Background tasks](https://keelson.dev/docs/building-apps/background-tasks/) for how to use tasks. | Code | Meaning | When it occurs | Resolution | |---|---|---|---| | `TASK_INVALID_REQUEST` | The task name is empty, `idempotency_key` is not 1–128 printable ASCII characters, the payload cannot be serialized to JSON, or the request is otherwise malformed | `enqueue`, on Keelson and locally | Fix the arguments. Retrying the same call does not help | | `TASK_NOT_DECLARED` | The task name is not in the currently deployed `tasks` (locally: not in `keelson.yaml`), including a name removed from `tasks` | `enqueue`, on Keelson and locally | Declare the task in `tasks` and deploy, or correct the name | | `TASK_NOT_FOUND` | The `task_id` does not exist or belongs to another app. Locally, `get` knows only tasks enqueued in the same process | `get`, on Keelson and locally | Check the `task_id` your app saved | | `TASKS_UNAVAILABLE` | Task intake is closed, or the app is suspended, quarantined, or being deleted. Retrying does not help | `enqueue`, on Keelson only | Check the app's status in the console. For a restricted app, contact support | | `TASK_PAYLOAD_TOO_LARGE` | The request body, `payload` and `idempotency_key` serialized as JSON, exceeds 65,536 bytes | `enqueue`, on Keelson and locally | Store large input in the database or with the Files SDK and pass its ID in `payload` | | `TASK_BACKLOG_LIMIT_EXCEEDED` | The app already has 1,000 tasks waiting to start (`queued`) | `enqueue`, on Keelson only | Wait until waiting tasks start, or enqueue less often. Tasks already enqueued are not affected | | `TASK_MONTHLY_QUOTA_EXCEEDED` | The workspace's monthly Background Jobs allowance cannot cover the first attempt | `enqueue`, on Keelson only | Intake can reopen within the same month when reserved allowance is returned, on a plan upgrade, or at the start of the next UTC month. Do not assume it stays closed until next month. The SDK does not retry it automatically | | `TASKS_UNAVAILABLE_TRANSIENT` | A temporary outage: 502 / 503 / 504 without an error body, a connection failure, or the 15-second timeout per call | `enqueue` and `get`, on Keelson only | The SDK retries up to 3 attempts in total. `enqueue` retries only with an `idempotency_key`; add one so a retry does not create a duplicate task | | `TASKS_UNAUTHORIZED` | The runtime API rejected the identity token (401) | `enqueue` and `get`, on Keelson only | Contact support if it persists | | `TASKS_FORBIDDEN` | Cloud Run rejected the call (403). Right after the first deploy that declares `tasks`, the permission can take a few minutes to take effect | `enqueue` and `get`, on Keelson only | Wait a few minutes after the first deploy with `tasks`, then try again. Contact support if it persists | | `TASKS_SERVER_ERROR` | Another 5xx response without an error body | `enqueue` and `get`, on Keelson only | Retry later. Use an `idempotency_key` when retrying `enqueue` | | `TASKS_HTTP_ERROR` | Another unsuccessful response without an error body | `enqueue` and `get`, on Keelson only | Contact support if it persists | | `TASKS_UNEXPECTED_RESPONSE` | A successful response did not have the documented shape | `enqueue` and `get`, on Keelson only | Update the SDK. Contact support if it persists | | `TASKS_IDENTITY_TOKEN_ERROR` | No identity token could be obtained from the metadata server | `enqueue` and `get`, on Keelson only | Contact support if it persists | | `TASKS_NOT_CONFIGURED` | The SDK could not decide between Keelson and local execution, for example because `KEELSON_MODE` is unknown, or `KEELSON_MODE=keelson` is set without `KEELSON_TASKS_BASE_URL` or `KEELSON_APP_ID` | `enqueue` and `get` | On Keelson, do not override `KEELSON_MODE` or the other `KEELSON_` variables. Locally, set `KEELSON_MODE=local` or remove the Keelson variables. See [Environment variables](https://keelson.dev/docs/reference/environment-variables/) | | `TASKS_LOCAL_CLI_NOT_FOUND` | `keelson` is not on `PATH` | `enqueue`, locally only | Install the CLI with `https://keelson.dev/install.sh` | | `TASKS_LOCAL_CLI_FAILED` | The CLI could not run the task, for example because it is too old to have `keelson dev task run`, or it was interrupted | `enqueue`, locally only | Run `keelson upgrade`. Run `keelson dev task run --payload ''` to see the error directly | A command that exits nonzero or times out is not an `enqueue` error. The task ends as `failed`, and `get` reports the reason in `last_failure_code`. ### Task failure reasons (`last_failure_code`) `get` returns `last_failure_code` for a `failed` task, and for a task that is waiting to be retried. The app's **Background Tasks** tab in the console shows the standard error of the last attempt. | Code | Meaning | Attempt counted | Retried | |---|---|---|---| | `exit_nonzero` | The command exited with a nonzero code | Yes | Yes, while attempts remain | | `timed_out` | The attempt exceeded `timeout` and Keelson stopped the command | Yes | Yes, while attempts remain | | `no_completion` | The attempt ended without reporting its result before the deadline | Yes | Yes, while attempts remain | | `delivery_failed` | The attempt could not start within one hour | No | No | | `manifest_unresolvable` | The task waited across two or more deploys, and the declaration it was enqueued with is no longer available | No | No | | `quota_exhausted` | A retry, or an attempt starting in a new month, could not reserve the monthly Background Jobs allowance | No | No | There is no manual rerun. To run the work again, enqueue it again from the app. ## Backup and restore These errors come from `keelson snapshots ...`. Codes starting with `snapshot_export_` occur in `snapshots export`. | Code | Meaning | Resolution | |---|---|---| | `snapshot_plan_not_included` | Plan does not include manual snapshots (all plans currently include them, so this should not normally occur) | Contact support | | `snapshot_no_managed_db` | App does not use Managed SQLite | Deploy with `db.mode: libsql` to create a DB | | `snapshot_db_name_invalid` / `snapshot_db_name_unknown` | Invalid or nonexistent DB name | Use a name shown in `hint` | | `snapshot_already_running` / `snapshot_export_already_running` | Previous operation is running | Wait for it to finish, then retry | | `snapshot_rate_limited` / `snapshot_export_rate_limited` | Request count limit reached | Wait until the time in `hint` | | `snapshot_export_invalid_timestamp` | Timestamp is outside the restorable range | Choose a timestamp within the range | | `snapshot_app_deleting` / `snapshot_export_app_deleting` | App is being deleted | — | | `snapshot_launch_failed` / `snapshot_export_launch_failed` | Operation could not start | Retry; contact support if it persists | | `snapshot_disabled` / `snapshot_storage_unconfigured` (also applies to corresponding `snapshot_export_` codes) | Feature is disabled or storage is unconfigured | Contact support | | `snapshot_not_ready` | Download is not ready (`X-Keelson-Error-Code` header) | Wait for completion | ## CLI errors | Code | Meaning | Resolution | |---|---|---| | `not_logged_in` | Not logged in | Run `keelson login` | | `multiple_workspaces` / `workspace_ambiguous` / `workspace_not_found` | Workspace cannot be identified | Supply `--workspace `; search with `keelson workspaces list --query ` | | `no_workspaces` | No available workspace | Create a workspace in the console | | `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 ` | | `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_expired` / `confirmation_rejected` | Approval expired or was rejected; expiry is 15 minutes for delete / SQL / restore and 30 minutes for `preview` / `app curl` | Run the command again | | `deploy_failed` | Deploy failed | Run `keelson diagnose ` | | `sql_failed` | SQL in `db apply` failed and was rolled back | Correct the SQL | | `usage` (`access set`; API 409 `detail.reason` is `last_manage_binding_removed`, `no_effective_manager`, or `self_lockout`) | Change would remove all manage groups, all effective managers, or your own permission | Retain a manage group. `--allow-self-lockout` only permits removing yourself; it cannot override the requirement for manage groups and effective managers | | `transient` | Temporary network error | Specify a retry count, for example `--retry 3` | **Legacy code compatibility:** Some workspace-related errors include the former `tenant` code in `error.aliases`. No removal date is set. ### When browser approval is required `confirmation_required` means the operation needs browser approval. Open the displayed URL and approve it. - Delete, SQL, and restore execute when approved; do not rerun the command. - For `preview` and `app curl`, rerun the same command after approval with `--confirmation `. ### Agent Skill update notice `meta.skill_outdated`, or a `skill_outdated` warning from `keelson doctor`, means the installed Skill is older than the CLI. This warning alone does not fail the command. Update with `keelson install-agent --yes`. ## Errors when accessing a deployed app These messages appear when opening a deployed app. They are not error screens for the console itself. | Display | State | Resolution | |---|---|---| | Login screen | Not logged in | 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) | IP controls denied access; the page shows the source IP | Ask an administrator whether the displayed IP should be allowed | | "The app cannot start right now because no app slot is available." (503) | Concurrent app limit reached | A slot generally becomes available after another app occupying one receives no access for about 5 minutes. If none becomes available, ask an administrator about stopping an app or changing plans | | "Starting the app" | Waking from sleep | Wait for the page to change automatically | | "This application has been suspended" (503) | Suspended by an administrator or restricted by Keelson | Ask the administrator why. Contact support if the administrator cannot resume it | | "App not found" (404) | No app matches the URL: incorrect URL, suspended or deleted app, or a newly created app awaiting publication | Check the URL and app status. For a newly created app, wait briefly | | "This URL is unavailable" (410) | App URL changed; the old URL is reserved for 30 days | Ask the administrator for the current URL | For API or XHR requests denied by the gateway, the JSON `detail` and `X-Keelson-Auth-Error` header contain reasons such as `ip-restricted` or `app-view-denied`. Capacity and suspension errors use `x-keelson-platform-error` with `capacity-full` or `app-suspended`. ### HTTP errors from app requests These errors occur during requests or connections to the app. In particular, a WebSocket 501 is not itself shown as a browser explanation page; what users see depends on the app's error handling. | HTTP status | Cause | Action for the app developer | |---|---|---| | 504 | App did not start returning response headers within 120 seconds; this is not the total streaming duration limit | Reduce work before the response starts. Use `crons` if the work can run at a scheduled time or interval | | 501 | Keelson does not support WebSocket | Use SSE or polling | ## External integrations (app tokens / Webhook) 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. At the gateway, a missing app route and a token belonging to another app both produce `machine-forbidden` or `webhook-forbidden`. If the edge cannot find the URL route before the request reaches the gateway, the response is 404 instead. | `X-Keelson-Auth-Error` | Cause | |---|---| | `invalid-app-token` | The token is invalid or revoked | | `machine-scope-denied` | Token lacks the `api` scope | | `machine-endpoint-denied` | The path or method is not declared in `auth.endpoints` | | `machine-forbidden` / `webhook-forbidden` | Gateway cannot find the app route, or the token belongs to another app | | `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 | Webhooks sent to platform-reserved paths such as `/api/webhooks/email` are rejected at the edge before reaching the gateway, with HTTP 403 and a plain-text body. The header is `x-keelson-platform-error: auth-webhook-endpoint-blocked`, not `X-Keelson-Auth-Error`. --- # CLI usage telemetry What the Keelson CLI collects and how to turn it off URL: https://keelson.dev/docs/reference/telemetry/ Language: en --- Keelson uses CLI usage telemetry to understand which workflows succeed, find reliability problems, and improve the CLI. It is enabled by default. ## What is collected - Subcommand name - 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. ## What is not collected - Your app's source code or the contents of deployment archives - The contents of logs, standard output, or standard error - Environment variable values or values passed to flags - File paths, workspace names, or app names - The contents of `keelson.yaml` - Conversations with an AI agent ## Turn telemetry off To save your preference, run: ```sh keelson telemetry off ``` To disable telemetry through the environment instead, set: ```sh 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/`. ## Notice before the first transmission 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. ## How this differs from `keelson feedback` `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.