# Basemodo documentation > Basemodo is a cloud for small software: deploy a folder with one command, or one sentence to your agent, get back a URL, and share it with the people it is for. Every App on Basemodo gets its own machine with a disk that keeps its data, a SQLite database ready to use, and sign-in for its visitors. Basemodo tells your App who is visiting, so it never needs login code of its own. It sleeps when nobody uses it and wakes on the next visit. A new App is private: only you can open it until you share it. - **New here?** [Getting started](https://basemodo.com/docs/getting-started) takes you from a folder on your computer to a link you can send, in plain words. - **Working with an agent** (Claude Code, Codex, Cursor)? [Deploying with an agent](https://basemodo.com/docs/agents) sets it up in one line. - **Looking for a command?** [The CLI](https://basemodo.com/docs/cli) lists every one, each with its own page and examples. The manifest, [`basemodo.toml`](https://basemodo.com/docs/manifest), has every key on one page. - **Got an error?** Every error names its fix and links to [its explanation](https://basemodo.com/docs/errors). This documentation is also in your terminal, as `basemodo docs`, and in one file for agents at [/llms.txt](https://basemodo.com/llms.txt). All three are written from the same source as the CLI's own `--help`, so they always agree. ## Start here - [Getting started](https://basemodo.com/docs/getting-started): From a folder on your computer to a link you can send to your team, in five steps and plain words. No servers, databases or login code to set up. - [Deploying with an agent](https://basemodo.com/docs/agents): Let Claude Code, Codex, Cursor or any agent deploy, share and fix your Apps for you, with one line of setup and your own sign-in. ## Guides - [Deploying](https://basemodo.com/docs/deploying): What happens when you deploy a folder: what is sent, how Basemodo works out how to build and run it, when it goes live, and what happens when it fails. - [Sharing](https://basemodo.com/docs/sharing): Who can open an App: its Visibility, the people you invite, and how they sign in. - [Who is visiting: identity headers](https://basemodo.com/docs/identity): How your App knows who is using it without any login code: Basemodo signs visitors in and sends their email, id and role with every request. - [Secrets](https://basemodo.com/docs/secrets): Keep API keys, tokens and passwords out of your code: set them as Secrets, and your App gets them as environment variables. - [Data: the disk, SQLite, db, run and Backups](https://basemodo.com/docs/data): Where your App keeps its data so it survives every Deploy and every sleep, the SQLite database every App already has, and how to query it or run a script where the data is. - [Public Paths: webhooks](https://basemodo.com/docs/public-paths): Let Stripe, Trello, GitHub or any other system call your App without signing in, on the few paths you name, while the rest of the App stays protected. - [Workers and cron](https://basemodo.com/docs/workers-and-cron): Run code besides the web: a worker that runs all the time (a queue, a bot), or a cron entry that runs on a schedule (a nightly report), declared in the manifest. - [Deploying from GitHub](https://basemodo.com/docs/github): Link an App to a GitHub repository and every push to its branch deploys it, with no terminal involved. - [Domains](https://basemodo.com/docs/domains): Where an App lives: its address on basemodo.app, your own domain with HTTPS, and what happens to links when you rename it. ## Concepts - [How Apps run](https://basemodo.com/docs/how-apps-run): What an App is on Basemodo: one machine with its own disk, the port it must answer on, how it sleeps and wakes, and what it may reach. - [Roles](https://basemodo.com/docs/roles): Who can do what with an App: its Owner, its Members, its Editors, and the Admins of a Workspace. - [Plans](https://basemodo.com/docs/plans): What you pay for: three Plans at flat prices with hard limits, a seven-day Trial to start, and never a surprise bill. ## Reference - [The basemodo CLI](https://basemodo.com/docs/cli): Every command of basemodo, the program that deploys and shares your Apps from a terminal, a script or an agent, with how it signs in and how it prints. - [The manifest: basemodo.toml](https://basemodo.com/docs/manifest): Every key of basemodo.toml, the optional file that names your App and says what detection cannot know: its start command, runtime versions, workers, cron entries and Public Paths. - [Supported runtimes](https://basemodo.com/docs/runtimes): What Basemodo recognizes in a folder with no configuration, how it builds and starts each, and what to do when your App is something else. - [Errors](https://basemodo.com/docs/errors): Every error Basemodo can answer, by its code: what it means and what to do. Each error links here, to its own entry. --- # Getting started From a folder on your computer to a link you can send to your team, in five steps and plain words. No servers, databases or login code to set up. You built something: a tracker for your team, a page of numbers, a small tool. Maybe an AI agent wrote it for you. It works on your computer, and now you want other people to use it. That is what Basemodo does. If you work with an agent, you can also let it do all of this for you: see [Deploying with an agent](https://basemodo.com/docs/agents). ## What you need - The folder with your App in it. Basemodo recognizes most web projects by themselves: a website (a folder with an `index.html`), a Node.js app (Next.js, Vite, Astro, Express, Hono), a Python app (Django, FastAPI, Flask, Streamlit), or anything with a `Dockerfile`. The full list is on [Supported runtimes](https://basemodo.com/docs/runtimes). - The `basemodo` command on your computer, and a terminal to type it in. On a Mac the terminal is the app called Terminal; on Windows, PowerShell. You only ever type the commands on this page. To check the command is there, type this and press Enter: ```sh basemodo --version ``` It answers with its version, like `basemodo 0.1.0`. ## 1. Sign in ```sh basemodo login ``` Your browser opens on Basemodo. Sign in with your email (Basemodo sends you a link, no password), or with Google or GitHub, then approve this computer. The terminal says who you are signed in as. You do this once per computer: it stays signed in for 30 days. ([`basemodo login`](https://basemodo.com/docs/cli/login) has the details, like signing in over SSH.) ## 2. Go to your folder In the terminal, move into the folder of your App. If it is in your Documents folder and called `lunch-rota`: ```sh cd Documents/lunch-rota ``` ## 3. Deploy ```sh basemodo deploy ``` Basemodo packs the folder, sends it, says what it found in it and why (the files it looked at), builds it and starts it. When it is ready, the last line gives your App's address: `Live at https://lunch-rota.basemodo.app`. The App is named after the folder; to choose another name, add `--name`, as in `basemodo deploy --name team-lunch`. If something goes wrong, the error says what happened and, on a line starting with `fix:`, exactly what to do; the `docs:` line links to the explanation. Your previous version, if there was one, keeps running. See [Deploying](https://basemodo.com/docs/deploying) for everything a Deploy does. ## 4. Open it Open the address in your browser. Basemodo asks you to sign in first: a new App is **Private**, so nobody but you can open it. That protection is Basemodo's, not your App's, so your App needs no login of its own. ## 5. Share it Invite people by email: ```sh basemodo share ana@example.com ben@example.com ``` Each of them gets an email with the address. They sign in with that email (or Google, or GitHub, if it uses the same address) and they are in. To let in anyone who has the link and signs in, or everyone without signing in: ```sh basemodo share --visibility link basemodo share --visibility public ``` [Sharing](https://basemodo.com/docs/sharing) explains each choice. You can do the same from the App's page on basemodo.com. ## Changing it Edit your App, then run `basemodo deploy` again in the same folder. Basemodo builds the new version, and switches visitors to it only once it works. If the new version turns out wrong, [`basemodo rollback`](https://basemodo.com/docs/cli/rollback) brings an earlier one back. What your App saves on its disk (in `/data`, where its database lives) stays through every Deploy: see [Data](https://basemodo.com/docs/data). ## What next - Something keeps secrets, like an API key? Keep them out of your code with [Secrets](https://basemodo.com/docs/secrets). - Want to see what your App prints, or whether it is up? [`basemodo logs`](https://basemodo.com/docs/cli/logs) and [`basemodo status`](https://basemodo.com/docs/cli/status). To open it in your browser, [`basemodo open`](https://basemodo.com/docs/cli/open); to start it again, [`basemodo restart`](https://basemodo.com/docs/cli/restart). - Want an agent to do all this for you? [Deploying with an agent](https://basemodo.com/docs/agents). - Every command, with examples: [the CLI](https://basemodo.com/docs/cli). --- # Deploying with an agent Let Claude Code, Codex, Cursor or any agent deploy, share and fix your Apps for you, with one line of setup and your own sign-in. Basemodo is built to be driven by agents. Every command answers in JSON, every error names the exact fix, and the `basemodo` command serves itself to your agent as an MCP server (the local MCP), so the agent calls Basemodo's commands as tools instead of guessing at a terminal. ## Set it up 1. Sign in once, yourself, in a terminal: `basemodo login` (see [Getting started](https://basemodo.com/docs/getting-started)). The agent uses this sign-in; it never sees a password. 2. Add Basemodo to your agent: **Claude Code** ```sh claude mcp add --scope user basemodo -- basemodo mcp ``` **Codex** ```sh codex mcp add basemodo -- basemodo mcp ``` **Cursor**: add this to `~/.cursor/mcp.json`: ```json {"mcpServers": {"basemodo": {"command": "basemodo", "args": ["mcp"]}}} ``` Any other agent that speaks MCP: run the command `basemodo mcp` as a stdio server. 3. Ask it. For example: - "Deploy this folder to Basemodo and give me the link." - "Share it with ana@example.com and ben@example.com." - "The App is broken: read its logs and fix it." - "Add a `done` column to the tasks table of the App's database." - "Set the Stripe key as a Secret; here it is: ..." ## An agent in the cloud: the remote MCP An agent that runs somewhere without the `basemodo` command (a web app like claude.ai, a hosted coding agent) connects to Basemodo's MCP over the internet instead, at `https://mcp.basemodo.com`, and signs in as you in your browser: **Claude Code** ```sh claude mcp add --transport http --scope user basemodo https://mcp.basemodo.com ``` then `/mcp` in a session to sign in. **Codex** ```sh codex mcp add basemodo --url https://mcp.basemodo.com codex mcp login basemodo ``` **Cursor**: in `~/.cursor/mcp.json`: ```json {"mcpServers": {"basemodo": {"url": "https://mcp.basemodo.com"}}} ``` Any other client that speaks MCP over HTTP with OAuth works the same way: it finds Basemodo's sign-in from the MCP's answer, registers itself, and sends you to basemodo.com to approve it, naming itself and where it returns. Approve only an agent you just connected. It then acts as you until you revoke it on the Devices page, where it is listed under Agents. Its tools are the same as the local MCP's, with two differences, since it has no folder of yours: tools that act on an App take the App's name (`app`), and `deploy` takes a GitHub repository (`repository`, as `owner/repo`, with `branch` and `subdir` if needed) instead of a path. The App is linked to the repository, its latest commit deployed, and every later push deploys too, as in [Deploying from GitHub](https://basemodo.com/docs/github); Basemodo's GitHub App must be able to read the repository, and when it cannot, the error's fix is the link to install it. ## What the agent can do Each command of the CLI is a tool of the same name (`env set` is `env_set`), with the same arguments, answering what the command prints with `--json`. [The CLI](https://basemodo.com/docs/cli) lists them all and says which ones only read (clients may run those without asking you) and which change things. A few stay in the terminal, like `basemodo login`, which needs you at your browser: the agent asks you to run it. The MCP also serves a guide, the resource `basemodo://guide`: the commands, the manifest and the identity headers in one page, written for agents. The agent reads it once and knows how to use Basemodo without searching the web. For more, the tool `docs` reads any page of this documentation. ## An agent without MCP An agent that only runs terminal commands uses the same CLI: tell it to add `--json` to every command. To teach it the rules, give it `basemodo docs`, or point it at [/llms.txt](https://basemodo.com/llms.txt): the whole documentation in one Markdown file, the same text as these pages. ## The rules an agent follows - **The CLI.** `basemodo `. Every command takes `--json`: the result is one JSON document on stdout, an error is `{"error": {"code", "message", "fix", "docs"}}` on stdout with a non-zero exit, and a command that takes a while reports progress as JSON lines on stderr (`{"event": "progress", "stage", "message"}`). The `fix` names the exact command or `basemodo.toml` line that fixes the error, and `docs` the page that explains it. - **The local MCP.** `basemodo mcp` serves the guide (the resource `basemodo://guide`) and one tool per command, signed in with the CLI's login. A tool's name is the command's words joined by `_` (`env set` is `env_set`); its arguments are the command's, named as the guide and the docs name them; its result is exactly what the command prints with `--json`, and a failure is an error result carrying the same `{"error": ...}`. A Deploy's stages arrive as MCP progress. Add it with `claude mcp add --scope user basemodo -- basemodo mcp` (Claude Code), `codex mcp add basemodo -- basemodo mcp` (Codex), or `{"mcpServers": {"basemodo": {"command": "basemodo", "args": ["mcp"]}}}` in `~/.cursor/mcp.json` (Cursor). - **Signing in.** `basemodo login`, run by the person in a terminal, signs the CLI and the local MCP in. When a tool answers `not_signed_in`, ask the person to run it. - **Which App.** Commands that act on an App take `app` (`--app`); without it, the App named after the current folder, as `deploy` names it. A name means the person's own App of that name, else the one other App of that name they work on (a Workspace's, or one they are an Editor of); an App's slug (the label in its URL) names it when the name is ambiguous. - **Owners and Editors.** The person signed in may work on their own Apps and on the Apps whose Owner made them an Editor: deploy, logs, Secrets (`env`), status and rollback, `backup` and `backups` work the same on both. Only the Owner changes who can reach an App (`share`), restores a Backup (`restore`, which overwrites its data), downloads its data or deletes it; an Editor gets `owner_only`. Each App's `role` says which it is. - **Workspaces.** An App is owned by a person or by a Workspace (a team). Every Admin of a Workspace manages all of its Apps (deploy, Logs, Secrets, sharing) and the App hears them as `owner`; its other Members do not manage them, unless made Editors. `deploy --workspace ` deploys to that Workspace's App, created the first time (only an Admin can). People, company domains and moving a person's App into a Workspace (which an Admin accepts; whoever moved it stays its Editor) are managed on basemodo.com, not here. - **The documentation.** Everything here and more, with examples: the tool `docs` (`basemodo docs`, one page with a topic: `basemodo docs manifest`), or https://basemodo.com/llms.txt. ## Staying in control The agent acts as you, with your sign-in: it can do what you can do. You see every device signed in as you on the Devices page of basemodo.com, and revoking the CLI's sign-in (or, for an agent using the remote MCP, the agent) there stops the agent at once. Tools that change things carry no "read only" mark, so a careful client asks you before running them. --- # Deploying What happens when you deploy a folder: what is sent, how Basemodo works out how to build and run it, when it goes live, and what happens when it fails. A Deploy is one published version of an App. You make one with [`basemodo deploy`](https://basemodo.com/docs/cli/deploy) in the App's folder (or an agent makes one with the `deploy` tool). The first Deploy creates the App. ## What is sent The CLI packs the folder and uploads it. It leaves out `.git` and everything your `.gitignore`, `.ignore`, `.basemodoignore` and `.dockerignore` files exclude, so dependencies like `node_modules` and build output stay home (Basemodo installs and builds on its own machines, never on yours). A folder may pack to at most 50 MB; add a `.basemodoignore` (same syntax as `.gitignore`) to leave out large files you do not need. ## How Basemodo knows what to build Basemodo looks at the files and says what it found and why: "Detected Next.js 15 (next.config.ts, package.json dependencies.next)". That detection decides how to install, build and start the App. [Supported runtimes](https://basemodo.com/docs/runtimes) lists what it recognizes and what each becomes. In short: 1. A `Dockerfile` at the top of the folder wins: Basemodo builds it as it is. 2. Otherwise a framework it knows (Next.js, Django...), then a runtime it knows (Node, Python), then a static website (an `index.html`). 3. Otherwise the Deploy stops with [`nothing_detected`](https://basemodo.com/docs/errors#nothing_detected), and the fix names the file to add. When detection cannot know something (the command that starts your App, the Node version you need, the App's name), say it in the manifest, a small `basemodo.toml` at the top of the folder: see [the manifest](https://basemodo.com/docs/manifest). What the manifest changes is listed next to the detection's explanation, each with its line. ## The stages `basemodo deploy` reports each stage as it happens (an agent receives them as progress): - **upload**: the folder is packed and sent. - **detect**: what Basemodo found, and what the manifest changed. - **queued**: another Deploy of this App is still running; Deploys of one App run one at a time, in the order they were asked for, so two in a row end with the latest one live. - **build**: the image is built on Basemodo's builders. Its log goes into the App's [Logs](https://basemodo.com/docs/cli/logs). - **start**: the new version starts on the App's machine. - **ready**: it answered HTTP, so visitors now reach it. ## When it is live Your App must listen for HTTP on the port in the `PORT` environment variable, on every interface (`0.0.0.0`), and answer within 60 seconds of starting. Any answer counts, even an error page. Detected Apps do this by themselves; an App started by your own command or `Dockerfile` must do it itself. [How Apps run](https://basemodo.com/docs/how-apps-run) explains this Port Contract. ## When it fails A failed Deploy changes nothing for your visitors: the previous version keeps running. The error says why, with the fix: - [`build_failed`](https://basemodo.com/docs/errors#build_failed): the build stopped; the error carries the build log, so you (or your agent) can read what broke. - [`not_ready`](https://basemodo.com/docs/errors#not_ready): it built and started, but never answered HTTP on `$PORT` within 60 seconds. - [`start_failed`](https://basemodo.com/docs/errors#start_failed): it could not start at all. - [`invalid_manifest`](https://basemodo.com/docs/errors#invalid_manifest): `basemodo.toml` has a mistake; the error names the key and the line. You are also told by email and in your notifications on basemodo.com. ## Going back [`basemodo rollback`](https://basemodo.com/docs/cli/rollback) makes an earlier Deploy live again, as a new Deploy that runs that Deploy's image: nothing is rebuilt, and it takes seconds. It uses the App's Secrets as they are now. The App's page on basemodo.com lists its Deploys, with their ids; `basemodo deploy --json` prints the id of the Deploy it made. ## Names and addresses An App's name is the folder's name, or `name` in `basemodo.toml`, or `--name`: lowercase letters, digits and dashes, at most 40. Its address is `https://.basemodo.app`, with a short random suffix (`lunch-rota-3f9a`) only when another App already has that name. Apps live on `basemodo.app`, a domain apart from basemodo.com, so an App can never read your basemodo.com sign-in. --- # Sharing Who can open an App: its Visibility, the people you invite, and how they sign in. Basemodo stands in front of every App. When someone opens it, Basemodo decides whether to let them in, signs them in if needed, and tells your App who they are (see [Who is visiting](https://basemodo.com/docs/identity)). Your App never implements login, invitations or passwords. You share from the terminal with [`basemodo share`](https://basemodo.com/docs/cli/share), from the Sharing section of the App's page on basemodo.com, or by asking your agent. ## Visibility An App's Visibility says who gets in. Every new App starts Private. ### Private Only the Owner, the Members and the Editors (the people you invited). Anyone else sees a sign-in page and, once signed in, a page saying they have no access. The safe default for anything internal. ```sh basemodo share --visibility private ``` ### Link Anyone who has the address and signs in, with any email. Good for a group you do not want to list one by one. Signing in still matters: your App learns who each visitor is. The first time someone you did not invite opens it, Basemodo asks them to confirm they want to continue to your App as their email. ```sh basemodo share --visibility link ``` ### Public Everyone, with no sign-in: a landing page, a public tool. Nobody is asked to sign in, so most visitors arrive without an identity and your App must work for them; a visitor who signs in to the App anyway arrives with theirs. ```sh basemodo share --visibility public ``` ### Workspace Everyone in the [Workspace](https://basemodo.com/docs/plans#workspace) that owns the App, with no list to keep: people who join the Workspace (by invitation, or by signing in with an email at its verified domain) get in, and people taken out of it lose access on their next request. Someone only invited to the Workspace is not in it until they join. The Owner, the Members and the Editors you invited still get in, so you can let in someone from outside too. Only for Apps a Workspace owns. Your App learns who each visitor is, as always; someone let in only because they are in the Workspace arrives with no `X-Basemodo-Role`, as on a Link App. Nobody in the Workspace is asked to confirm before continuing to the App: it is their own Workspace's. Anyone else signed in sees a page saying the App is for the people of its Workspace, with a link to request access. ```sh basemodo share --visibility workspace ``` ## Inviting people ```sh basemodo share ana@example.com ben@example.com ``` Each email gets a message with the App's address. The invitation is pending until they open the App signed in with that email (by a sign-in link, or Google or GitHub with the same address); then they are a Member, and you are told. Inviting someone again sends the email again. See who has access, and who has not accepted yet: ```sh basemodo share ``` Remove a Member, or take back an invitation; it takes effect at their next request: ```sh basemodo share --remove ben@example.com ``` ## Editors An Editor is a Member who may also work on the App from their own account: deploy it, roll it back, read its Logs, change its Secrets, run commands and SQL. The developer helping you, without your password or your bill: ```sh basemodo share --editor dev@example.com ``` Editors cannot change who reaches the App; only the Owner shares it. See [Roles](https://basemodo.com/docs/roles#editor). ## Access Requests Someone signed in who reaches a Private App they are not invited to sees a page saying so, with a button to ask for access. You are told by email and in your notifications, and answer in one click on the App's page, or from the terminal: ```sh basemodo share # lists who asked basemodo share --approve carla@example.com # makes her a Member basemodo share --decline carla@example.com ``` ## How visitors sign in Visitors sign in on basemodo.com, by a link emailed to them (no password), or with Google or GitHub. One email is one person, however they sign in. Their sign-in lasts 30 days on that device, and it opens every App shared with them. Opening an App from basemodo.com, or with [`basemodo open`](https://basemodo.com/docs/cli/open), signs you in to it on the way, even to a Public App that asks nobody to sign in. So you arrive as yourself, with your Toolbar. ## The Toolbar On an App's pages, Basemodo shows signed-in visitors its Toolbar: a small floating pill with who is here now, the App's Visibility, and a way to share it (Share for the Owner, Copy link for everyone else). Drag it to another corner, or hide it with `⌘.` (`Ctrl+.` off a Mac); the browser remembers both for that App. Visitors who are not signed in, on a Public App, never see it. The Toolbar only shows. It never changes who can reach the App: Share and Visibility open a small window on basemodo.com, and the change is made there. It is on for every App. Turn it off with [`basemodo toolbar off`](https://basemodo.com/docs/cli/toolbar), from the App's page on basemodo.com, or with [`toolbar = false`](https://basemodo.com/docs/manifest#web.toolbar) under `[web]` in its manifest, which applies when that Deploy goes live. A Deploy without the line leaves the Toolbar as it is. Only the Owner turns it off or on. ```sh basemodo toolbar off ``` The Toolbar is a script Basemodo adds to the App's HTML pages, served from the App's own address at `/.basemodo/toolbar.js`. A Content Security Policy with `script-src 'self'` lets it in. A strict policy that allows only scripts carrying a nonce keeps it out: Basemodo never edits your App's policy, so that App shows no Toolbar. ## Search engines Apps are never indexed by search engines: Basemodo answers `/robots.txt` for every App with "disallow everything" and marks every page `noindex`, whatever the App says. A Public App can choose to be found, for a landing page: [`basemodo indexing on`](https://basemodo.com/docs/cli/indexing), or [`indexable = true`](https://basemodo.com/docs/manifest#web.indexable) under `[web]` in its manifest. Making the App anything but Public turns indexing off again. ## Webhooks Other systems (Stripe, Trello, GitHub) cannot sign in. To let them call your App, declare those paths as [Public Paths](https://basemodo.com/docs/public-paths). ## Who can change sharing Only the Owner changes the Visibility and the Members. [Roles](https://basemodo.com/docs/roles) explains what Owners, Members, Editors and Admins can each do. --- # Who is visiting: identity headers How your App knows who is using it without any login code: Basemodo signs visitors in and sends their email, id and role with every request. ## The headers Every request reaches an App through Basemodo's gate, never directly. A new App is Private: the gate shows visitors a sign-in page and lets through only the people the App is shared with. On every request from a signed-in visitor it tells the App who they are: - `X-Basemodo-Email`: their email, lowercase. - `X-Basemodo-Id`: their Basemodo id, stable even if their email changes; key your own records on it. - `X-Basemodo-Role`: what they are to the App (`owner`, `member`, or `editor` for a Member the Owner also lets deploy it), when they have a role (a visitor let in by a Link, Public or Workspace Visibility has none). For an App a Workspace owns, each of its Admins is an `owner`. - `X-Basemodo-Jwt`: the same, signed (EdDSA) with a key of this App, for an App that wants to verify it: claims `iss`, `aud` (the App's URL), `sub` (the id), `email`, `role`, `app`, `iat`, `exp` (a few minutes). Verify it against the public key in the App's environment, `BASEMODO_JWT_PUBLIC_KEY` (PEM). The gate removes every `X-Basemodo-*` header a client sends and never forwards its own cookies, so an App can trust these headers and must not implement login of its own. A visitor who has not signed in (on a Public App) arrives without them. Paths under `/.basemodo/` belong to the gate and never reach the App. Search engines index no App unless its Owner says so: the gate sends `X-Robots-Tag: noindex, nofollow, noarchive` on every answer and serves a `robots.txt` that disallows everything, replacing the App's own. Only for a Public App, `indexing on` (or `[web] indexable = true`) lets them in: the App's answers then leave as it sent them, and `robots.txt` lets crawlers in. Leaving Public turns it off. On the App's HTML pages, the gate shows signed-in visitors Basemodo's Toolbar: a small pill with who is here now, the App's Visibility and a way to share it, which only shows (who can reach the App is changed on basemodo.com). It is the script `/.basemodo/toolbar.js`, same-origin, so a Content Security Policy with `script-src 'self'` lets it in and a nonce-only policy keeps it out. It is on for every App; `toolbar off` (or `[web] toolbar = false`) turns it off. ## Reading them The headers arrive on every request, so "who is this?" is one line. Node with Express: ```js app.get("/", (req, res) => { const email = req.get("X-Basemodo-Email"); // "ana@example.com" const id = req.get("X-Basemodo-Id"); // keep your records under this const role = req.get("X-Basemodo-Role"); // "owner", "member", or undefined res.send(`Hello ${email ?? "stranger"}`); }); ``` Python with Flask: ```python from flask import Flask, request app = Flask(__name__) @app.get("/") def home(): email = request.headers.get("X-Basemodo-Email") is_owner = request.headers.get("X-Basemodo-Role") == "owner" return f"Hello {email or 'stranger'}" ``` Store your own data under `X-Basemodo-Id` rather than the email: the id stays the same if the person's email changes. Only the owner may see a settings page? Check the role: ```js if (req.get("X-Basemodo-Role") !== "owner") return res.status(403).send("Owners only"); ``` ## Verifying the signed token The headers are safe to trust as they are, because every request passes through Basemodo, which removes any `X-Basemodo-*` header a visitor sends. If you want proof anyway (your App also runs somewhere else, or you pass the identity on to another service), verify `X-Basemodo-Jwt` with the public key Basemodo gives your App in `BASEMODO_JWT_PUBLIC_KEY`. Node, with the `jose` package: ```js import { importSPKI, jwtVerify } from "jose"; const key = await importSPKI(process.env.BASEMODO_JWT_PUBLIC_KEY, "EdDSA"); async function visitor(req) { const token = req.get("X-Basemodo-Jwt"); if (!token) return null; // not signed in (a Public App) const { payload } = await jwtVerify(token, key, { issuer: "https://basemodo.com", audience: "https://lunch-rota.basemodo.app", // your App's address }); return { id: payload.sub, email: payload.email, role: payload.role }; } ``` Python, with PyJWT and `cryptography`: ```python import os import jwt KEY = os.environ["BASEMODO_JWT_PUBLIC_KEY"] def visitor(headers): token = headers.get("X-Basemodo-Jwt") if not token: return None claims = jwt.decode( token, KEY, algorithms=["EdDSA"], issuer="https://basemodo.com", audience="https://lunch-rota.basemodo.app", ) return {"id": claims["sub"], "email": claims["email"], "role": claims.get("role")} ``` The token lives a few minutes and is for your App's address only: a token sent to one App is refused by any other. ## Running your App on your own computer When you run your App locally, nothing sets these headers. Fall back to a fixed person while developing (for example `req.get("X-Basemodo-Email") ?? "me@localhost"` when `NODE_ENV` is not `production`), and never in a deployed App. ## Who gets in The headers say who is visiting; whether they get in at all is the App's Visibility and its Members, set by its Owner: see [Sharing](https://basemodo.com/docs/sharing). A visitor let in only because the App is Link or Public has no role. Requests to [Public Paths](https://basemodo.com/docs/public-paths) (webhooks) carry no identity unless the caller happens to be signed in to the App. --- # Secrets Keep API keys, tokens and passwords out of your code: set them as Secrets, and your App gets them as environment variables. A Secret is a value your App needs and nobody else should see. Basemodo keeps it encrypted, gives it to your App's processes as an environment variable, and never shows it again, not even to you: the list shows names, who set each, and when. ## Setting one ```sh basemodo env set STRIPE_KEY=sk_live_123 MAILER_TOKEN=abc ``` All the names in one command are set together, or none if one is wrong. To keep a value out of your shell's history, give the name alone and type or pipe the value: ```sh basemodo env set STRIPE_KEY < stripe-key.txt ``` Your App gets new or changed Secrets **the next time it starts**: the running version keeps the values it started with. Every start reads them again: a Deploy, waking from sleep, or a restart. So set them, then run [`basemodo restart`](https://basemodo.com/docs/cli/restart) (no build), or `basemodo deploy`. In your code they are ordinary environment variables: `process.env.STRIPE_KEY` in Node, `os.environ["STRIPE_KEY"]` in Python. ## Listing and removing ```sh basemodo env list basemodo env unset MAILER_TOKEN ``` `env list` shows each name with who set it and when, and the latest changes. The App's page on basemodo.com has the same list and a form to set and unset them. ## Rules - Names are letters, digits and `_`, not starting with a digit, at most 128 characters: `STRIPE_KEY`, `db_password_2`. - `PORT` and every name starting with `BASEMODO_` are Basemodo's, set for every App (see [How Apps run](https://basemodo.com/docs/how-apps-run)). - A value is at most 32 KB; an App has at most 100 Secrets. - Everyone who may deploy an App may change its Secrets, and every change is recorded with who made it. ## Commands - [`basemodo env set`](https://basemodo.com/docs/cli/env-set) - [`basemodo env list`](https://basemodo.com/docs/cli/env-list) - [`basemodo env unset`](https://basemodo.com/docs/cli/env-unset) --- # Data: the disk, SQLite, db, run and Backups Where your App keeps its data so it survives every Deploy and every sleep, the SQLite database every App already has, and how to query it or run a script where the data is. ## The disk Every App has its own disk, mounted at `/data` (also in `$BASEMODO_DATA`). What your App writes there stays: through every Deploy, every sleep, every restart. Everything else on the machine is replaced at the next Deploy, so never keep data next to your code. ## The database Every App has a SQLite database at `/data/app.db`, also in `$BASEMODO_DB`. There is nothing to provision or connect to: your App opens the file, and the first to open it creates it. SQLite is fast, needs no server, and is plenty for small software. Node (22 and later have SQLite built in): ```js import { DatabaseSync } from "node:sqlite"; const db = new DatabaseSync(process.env.BASEMODO_DB ?? "app.db"); db.exec("create table if not exists tasks (id integer primary key, title text, done integer default 0)"); const tasks = db.prepare("select * from tasks").all(); ``` Python: ```python import os import sqlite3 db = sqlite3.connect(os.environ.get("BASEMODO_DB", "app.db")) db.execute("create table if not exists tasks (id integer primary key, title text)") ``` Falling back to a local file name, as above, lets the same code run on your computer. With Django, point `DATABASES["default"]["NAME"]` at `os.environ.get("BASEMODO_DB")`. ## Looking at it: `basemodo db` Run SQL against an App's database from your terminal, or let your agent do it with the `db` tool: ```sh basemodo db "select * from tasks order by id desc limit 10" basemodo db "update tasks set done = 1 where id = 7" basemodo db < fix-titles.sql ``` It prints a table (or JSON with `--json`). Several statements run in order and stop at the first error. If the App is asleep, it is woken first. See [`basemodo db`](https://basemodo.com/docs/cli/db). ## Running a script: `basemodo run` Run a one-off command inside the App's machine, where its disk, its database and its Secrets are: a migration, an import, a quick look around. ```sh basemodo run "npm run migrate" basemodo run python manage.py migrate basemodo run -- ls -la /data ``` Its output comes back as it runs, and the command exits as the remote command did. It may run up to 60 seconds, or longer with `--timeout` (at most 15 minutes). See [`basemodo run`](https://basemodo.com/docs/cli/run). Basemodo never runs migrations by itself when a Deploy starts: run them with `basemodo run` when you choose, so a bad migration never runs twice. ## Backups: `basemodo backup` and `basemodo restore` Basemodo backs every App's data up once a day: the database and every file on its disk, kept 7 days (30 on Pro and Workspace, see [Plans](https://basemodo.com/docs/plans#logs-backups-and-cron-retries)). Take one yourself before something risky, then put the data back if it goes wrong: ```sh basemodo backup # prints the Backup's id basemodo run "npm run migrate" basemodo backups # the App's Backups, newest first basemodo restore # the data as it was when that Backup was taken ``` Restoring replaces everything written since the Backup; the Backup itself stays, and the App runs again afterwards. Your agent has the same `backup`, `backups` and `restore` tools. See [`basemodo backup`](https://basemodo.com/docs/cli/backup) and [`basemodo restore`](https://basemodo.com/docs/cli/restore). ## Downloading your data The App's page on basemodo.com has **Download the data**: a `.tar.gz` of everything on its disk, with the database copied by SQLite's own online backup, so it is consistent even while the App writes to it. Open `data/app.db` with any SQLite tool. ## Deleting an App Deleting an App (on its page, by its Owner) stops it at once, and its address stops answering. Its last Backup is kept for 7 days (30 on Pro and Workspace): `basemodo restore ` with the id the page shows brings the App back as it was. After that its data is gone for good and its address is free for someone else. --- # Public Paths: webhooks Let Stripe, Trello, GitHub or any other system call your App without signing in, on the few paths you name, while the rest of the App stays protected. A Private or Link App asks every visitor to sign in. A webhook cannot sign in, so it would only ever see the sign-in page. Public Paths are the exception: the paths you list in the manifest let anyone through, even on a Private App. ## Declaring them In `basemodo.toml` at the top of the folder, with [`public_paths`](https://basemodo.com/docs/manifest#public_paths): ```toml public_paths = ["/webhooks/stripe", "/hooks/*"] ``` Then deploy. They open with the Deploy that declares them, and close with the first Deploy that drops them. - A path is matched exactly: `/webhooks/stripe` opens that path only, not `/webhooks/stripe/other`. - `/*` at the end opens every path under it: `/hooks/*` opens `/hooks/trello` and `/hooks/a/b`. - A path with `.` or `..` segments, an empty segment, a backslash or an encoded `/` never matches, so nobody can reach another part of your App through a Public Path. ## What a request on a Public Path gets - It reaches your App without signing in, and **without identity headers**, unless the caller happens to be signed in to the App. So check who is calling yourself: most services sign their webhooks (Stripe's `Stripe-Signature`, GitHub's `X-Hub-Signature-256`); keep their signing secret in a [Secret](https://basemodo.com/docs/secrets) and verify every request. - Stricter limits than signed-in visitors: 60 requests at once, 600 a minute, and a body of at most 1 MB. Over them the caller gets `429` or `413`, and your App is not disturbed. A flood on a webhook never uses up the allowance of your signed-in visitors. - A sleeping App is woken by it, as by any visit. ## Seeing what is open [`basemodo status`](https://basemodo.com/docs/cli/status) lists the App's Public Paths, and so does the App's page on basemodo.com, so you always know exactly what is open. --- # Workers and cron Run code besides the web: a worker that runs all the time (a queue, a bot), or a cron entry that runs on a schedule (a nightly report), declared in the manifest. An App's code runs as Processes on its machine. There is always one `web` Process, which answers HTTP. You can add others in [`basemodo.toml`](https://basemodo.com/docs/manifest), and they share the same machine, disk, database and Secrets. ## Workers A worker runs continuously beside `web`, and is restarted if it stops. ```toml [[worker]] name = "mailer" command = "node mailer.js" ``` Repeat `[[worker]]` for several. A worker keeps the App awake: an App with one never sleeps, so workers come with the Plans that allow Apps to stay on (see [Plans](https://basemodo.com/docs/plans)). A worker that stops is restarted after a pause that doubles each time (1 second up to a minute). One that keeps crashing (eight quick exits in a row) is left stopped and the App is paused: you are told why, `basemodo status` says `paused` with the reason, and the next Deploy resumes it. Each line a worker prints reaches the [Logs](https://basemodo.com/docs/cli/logs) with its name in front: `[mailer] sent 3 emails`. Keys: [`worker.name`](https://basemodo.com/docs/manifest#worker.name), [`worker.command`](https://basemodo.com/docs/manifest#worker.command). ## Cron A cron entry runs a command on a schedule. ```toml [[cron]] name = "nightly-report" schedule = "0 3 * * *" command = "node report.js" timezone = "America/Bogota" timeout = "5m" ``` - `schedule` has five fields: minute, hour, day of the month, month, day of the week. `0 3 * * *` is every day at 03:00; `*/15 * * * *` every 15 minutes; `0 9 * * 1` Mondays at 09:00. - It is read in UTC unless you give a `timezone` (an IANA name, like `Europe/Madrid`). - A run may take 15 minutes unless you give a `timeout` (`30s`, `5m`, `2h`; at most `24h`); then it is stopped. - Runs never overlap: if the previous run is still going when the next is due, the next one is skipped (the Logs say so). - A sleeping App is woken for its run. - Each run's output and exit code go into the App's [Logs](https://basemodo.com/docs/cli/logs). - On Pro and Workspace Plans, a run that fails or times out is tried again up to 3 times, after 1, 2 and 4 minutes; the Logs say when (see [Plans](https://basemodo.com/docs/plans#logs-backups-and-cron-retries)). Repeat `[[cron]]` for several. Names must be unique among workers and cron entries, and cannot be `web`. Keys: [`cron.schedule`](https://basemodo.com/docs/manifest#cron.schedule), [`cron.timezone`](https://basemodo.com/docs/manifest#cron.timezone), [`cron.timeout`](https://basemodo.com/docs/manifest#cron.timeout), and the rest on [the manifest's page](https://basemodo.com/docs/manifest). --- # Deploying from GitHub Link an App to a GitHub repository and every push to its branch deploys it, with no terminal involved. Besides deploying a folder from your computer, an App can take its code from GitHub: a repository, a branch, and if the App lives in a part of the repository, its folder. Every push to that branch then makes a Deploy, exactly as `basemodo deploy` would: the same detection, the same [manifest](https://basemodo.com/docs/manifest), the same stages, and a failed one leaves the previous version running (see [Deploying](https://basemodo.com/docs/deploying)). ## Linking a repository Basemodo reads your repositories through Basemodo's GitHub App, which you install once on your GitHub account or organization, choosing which repositories it may read. Every App you link afterwards reuses it, so linking a second repository is instant. The quickest way is from a clone of the repository on your computer. [`basemodo deploy`](https://basemodo.com/docs/cli/deploy) notices the folder's GitHub remote and offers to link the App to that repository, the branch you are on and the folder you are in, then deploys as usual: ```sh basemodo deploy # asks whether to link basemodo deploy --link # links without asking basemodo deploy --no-link # never offers ``` If Basemodo's GitHub App cannot read the repository yet, it gives you the link to install it (or to give it that repository); then run the command again. With `--json`, the result's `github` says whether it linked, or gives the command or the install link. You can also link, change or unlink from the GitHub section of the App's page on basemodo.com, which lists each Deploy with the commit it came from. ## What a push does - A push to the linked branch deploys the commit pushed. Pushes to other branches do nothing, and for an App linked to a folder, neither do pushes that change nothing in it. - Pushes in a row queue: Deploys of one App run one at a time, so the latest push ends live. - A failed build is reported by email and in your notifications, with the build log in the App's [Logs](https://basemodo.com/docs/cli/logs). - Uninstalling the GitHub App, or taking the repository away from it, disconnects the App until it is given back; renaming or transferring the repository keeps the link. ## Secrets stay out of the repository Never commit keys or passwords. Set them with [Secrets](https://basemodo.com/docs/secrets): they reach the App at every Deploy, whether it came from a push or from your terminal. --- # Domains Where an App lives: its address on basemodo.app, your own domain with HTTPS, and what happens to links when you rename it. ## Its address Every App answers at `https://.basemodo.app`, its name followed by a short suffix only when another App already had that name. The address is on its own domain, apart from basemodo.com, so nothing an App does can touch your Basemodo account. ## Renaming an App [`basemodo rename`](https://basemodo.com/docs/cli/rename) gives an App a new name, and its address follows: ```sh basemodo rename team-lunch --app lunch-rota ``` The old address keeps working for 30 days: every request to it is sent to the new one, path and all, so links people already have still land. Meanwhile no other App can take the old address. Its [custom domains](#custom-domains) do not change. From then on, deploy it with `basemodo deploy --name team-lunch` (or from a folder of that name). Only the App's Owner renames it. ## Custom domains On the [Pro and Workspace Plans](https://basemodo.com/docs/plans) an App can also answer at a hostname of yours, like `app.example.com`, with HTTPS that Basemodo sets up and renews. On the Trial and Starter, adding one is refused with [`custom_domains_not_on_plan`](https://basemodo.com/docs/errors#custom_domains_not_on_plan). 1. Add it: [`basemodo domains add app.example.com`](https://basemodo.com/docs/cli/domains-add). It prints two DNS records. 2. Add both at your domain's DNS host: - a `CNAME` record at `app.example.com` pointing to `domains.basemodo.app`, which sends visitors to Basemodo; - a `TXT` record at `_basemodo-challenge.app.example.com` with the value it printed, which proves the domain is yours (the CNAME alone does not: anyone could have left one pointing at Basemodo). 3. Verify: [`basemodo domains verify app.example.com`](https://basemodo.com/docs/cli/domains-verify). DNS changes can take a few minutes to show; the error [`domain_not_verified`](https://basemodo.com/docs/errors#domain_not_verified) says which record is still missing. Basemodo also looks by itself every few minutes for a week. Once verified, the App answers at the hostname at once, with the same [Visibility](https://basemodo.com/docs/sharing) and sign-in as at its basemodo.app address, and the same rules for search engines. Basemodo then gets a certificate from Let's Encrypt (a minute or so): [`basemodo domains list`](https://basemodo.com/docs/cli/domains-list) shows each domain as `pending` (records not found yet), `issuing`, `active` (served over HTTPS) or `failed` (with the reason, for example a CAA record that does not allow Let's Encrypt; Basemodo tries again within the hour). Certificates are renewed well before they expire, as long as the CNAME still points at Basemodo. The CNAME cannot sit at the apex of a domain (`example.com` itself), so use a subdomain such as `www` or `app`. A hostname belongs to one App: the first to verify it holds it, and [`domain_taken`](https://basemodo.com/docs/errors#domain_taken) tells anyone else. [`basemodo domains remove`](https://basemodo.com/docs/cli/domains-remove) frees it; deleting the App does too. --- # How Apps run What an App is on Basemodo: one machine with its own disk, the port it must answer on, how it sleeps and wakes, and what it may reach. ## One App, one machine Every App runs on its own machine (a small virtual machine), with its own disk at `/data`. Its Processes share the machine: `web`, which answers HTTP, and any [workers and cron entries](https://basemodo.com/docs/workers-and-cron) you declare. No other App runs on it, and Apps cannot reach each other. Basemodo gives every Process these environment variables, besides your [Secrets](https://basemodo.com/docs/secrets): | Variable | What it holds | | --- | --- | | `PORT` | The port the `web` Process must listen on. | | `BASEMODO_DATA` | `/data`, the App's disk: what is written there survives every Deploy. | | `BASEMODO_DB` | `/data/app.db`, the App's [SQLite database](https://basemodo.com/docs/data). | | `BASEMODO_JWT_PUBLIC_KEY` | The key to verify [the identity token](https://basemodo.com/docs/identity) with. | Processes, and commands run with [`basemodo run`](https://basemodo.com/docs/cli/run) and [`basemodo db`](https://basemodo.com/docs/data), never run as root: they run as your image's own `USER`, or, when the image runs as root (every image Basemodo builds), as an unprivileged user of Basemodo's (uid 10000). That user owns `/data` (and, in images Basemodo builds, the App's folder) and may write in `/tmp`; it may listen on any port, 80 too. If your own `Dockerfile` writes elsewhere while it runs, give what it writes to its `USER` (`COPY --chown`, `RUN chown`), or set `USER` to the user that owns it: a Process denied permission gets a line in the Logs that says so. ## The Port Contract The `web` Process must listen on the port in `$PORT`, on every interface (`0.0.0.0`, not `localhost`), and answer HTTP. Its first answer, whatever it is, within 60 seconds of starting means the App is ready, and only then does a Deploy go live. There is nothing else to configure: no health check, no exposed port. Apps Basemodo detects keep this contract by themselves. If you start your App yourself (with [`web.command`](https://basemodo.com/docs/manifest#web.command) or your own `Dockerfile`), read the port from the environment, as in `app.listen(process.env.PORT ?? 3000, "0.0.0.0")`. `EXPOSE` in a Dockerfile does not change the port. An App that never answers fails its Deploy with [`not_ready`](https://basemodo.com/docs/errors#not_ready). ## Sleep and wake An App nobody uses for 10 minutes falls asleep: its machine stops, its disk stays, and it costs nothing. The next visit wakes it: Basemodo holds that request for the few seconds the App takes to start, then passes it on. Nobody sees an error. Only requests that get in count as use, so strangers hitting the sign-in page of a Private App never keep it awake, but a webhook on a [Public Path](https://basemodo.com/docs/public-paths) does wake it. [`basemodo status`](https://basemodo.com/docs/cli/status) says whether an App is awake, asleep or starting, or paused: stopped by Basemodo because a [worker](https://basemodo.com/docs/workers-and-cron) kept crashing (or by its Plan), with the reason, until it is deployed again or restarted. A worker keeps an App awake. [`basemodo restart`](https://basemodo.com/docs/cli/restart) starts an App's machine again on its live Deploy, without building: use it to apply [Secrets](https://basemodo.com/docs/secrets) you just set (every start reads them, waking from sleep included), or to resume an App paused because a worker kept crashing or ran out of memory or disk once you fixed the cause. An App paused until its Owner chooses a Plan, or for review, stays paused. ## Logs What your Processes print (stdout and stderr) and what each build prints are kept as the App's Logs for 7 days, 30 on Pro and Workspace ([Plans](https://basemodo.com/docs/plans)). Follow them live with [`basemodo logs`](https://basemodo.com/docs/cli/logs), or see the latest on the App's page. ## The network - **In**: only through Basemodo. Every request reaches the App through Basemodo's gate, which signs visitors in, applies the App's [Visibility](https://basemodo.com/docs/sharing), adds the [identity headers](https://basemodo.com/docs/identity) and limits floods. The machine has no public address of its own. - **Out**: open by default, so your App can call any API. Basemodo blocks itself (basemodo.com and its internal networks), the provider's metadata service, private network ranges, other Apps' machines (call another App at its URL instead) and outgoing email over SMTP ports 25, 465 and 587 (send email through an email API instead). Apps a Workspace owns can be limited to a list of hosts with [`network.allow`](https://basemodo.com/docs/manifest#network.allow); the blocks still apply to the hosts listed. A personal App's Deploy with `network.allow` is refused, since the allowlist comes with the [Workspace plan](https://basemodo.com/docs/plans). The App's names are resolved by Basemodo inside its machine: names it may not reach are refused there, and other DNS servers are not reachable. ## Addresses and search engines Apps answer at `https://.basemodo.app`, a domain apart from basemodo.com. Paths under `/.basemodo/` belong to Basemodo (the sign-in) and never reach your App. Apps are never indexed by search engines unless a Public App asks to be (see [Sharing](https://basemodo.com/docs/sharing#search-engines)). --- # Roles Who can do what with an App: its Owner, its Members, its Editors, and the Admins of a Workspace. ## The roles ### Owner The person (or [Workspace](https://basemodo.com/docs/plans#workspace)) an App belongs to. The Owner decides who can reach the App, pays for it, and can do everything: deploy, roll back, read the Logs, change Secrets, run commands and SQL, share it. Whoever deploys an App first owns it. Your App hears `X-Basemodo-Role: owner` for its Owner. ### Member A person the Owner shares the App with, by inviting their email (see [Sharing](https://basemodo.com/docs/sharing)). Members use the App: they pass Basemodo's sign-in and reach it, and nothing more. Your App hears `X-Basemodo-Role: member`. Someone let in only because the App is Link, Public or Workspace is not a Member, and your App hears no role for them. In a Workspace, Member also means a person who belongs to the Workspace. Each one is a seat on the Workspace's bill, not a payer: the Workspace pays. ### Editor A Member the Owner lets work on the App without owning it: the developer helping you, added with `basemodo share --editor EMAIL`. Editors deploy, roll back, read the Logs, change Secrets, run commands and SQL, and take Backups, from their own account. They do not change who can reach the App, restore a Backup (which overwrites its data), download its data or delete it (they get [`owner_only`](https://basemodo.com/docs/errors#owner_only)), and they do not pay for it. Your App hears `X-Basemodo-Role: editor`. ### Admin A Workspace Member who also manages the Workspace: its people, its bill, and every App the Workspace owns, including those Apps' Members and Editors: your App hears `X-Basemodo-Role: owner` for every Admin of the Workspace that owns it. Nothing in a Workspace depends on one employee. A Workspace always has at least one Admin. ## Who can do what | | Owner | Editor | Member | | --- | --- | --- | --- | | Open the App | yes | yes | yes | | Deploy, roll back, read Logs | yes | yes | no | | Change Secrets, run commands and SQL | yes | yes | no | | Take and list Backups | yes | yes | no | | Restore a Backup, download the data, delete the App | yes | no | no | | Change the Visibility, invite and remove Members | yes | no | no | | Pay for it | yes | no | no | The Admins of a Workspace can do everything an Owner can on every App the Workspace owns. ## Everyone is a Person Owners, Members, Editors and Admins are all Persons: one email is one Person, whether they sign in by email link, Google or GitHub, and whether they own Apps or were only ever invited to one. Members never pay. --- # Plans What you pay for: three Plans at flat prices with hard limits, a seven-day Trial to start, and never a surprise bill. Every Plan is a flat price per month with hard quotas. Nothing is metered: an App that reaches a limit is paused or its Deploy refused, with a message saying which limit and what to do, never an extra charge. Members of your Apps never pay. Prices are on basemodo.com and set when Basemodo launches; the limits below are the shape of each Plan. ## The Plans ### Starter For one App, at its basemodo.app address. A new account starts on Starter with a **Trial**: seven days, no card, one App that sleeps when unused. When the Trial ends without a card, the App pauses (it is not deleted) and its data is kept for 30 days; adding a card resumes it where it was. ### Pro For a person with several Apps: up to ten Apps, [workers](https://basemodo.com/docs/workers-and-cron) and Apps that stay awake, [your own domains](https://basemodo.com/docs/domains) with HTTPS (ten), 5 GB of disk per App, Logs and Backups kept 30 days, and failed cron runs retried. Priced per person. ### Workspace For a team or a company, priced per seat with one bill: everything in Pro, plus Apps owned by the Workspace (so they outlive the person who built them), [Workspace Visibility](https://basemodo.com/docs/sharing#workspace), [Admins](https://basemodo.com/docs/roles#admin), an outbound network allowlist per App ([`network.allow`](https://basemodo.com/docs/manifest#network.allow)), and a dedicated outgoing IP as an extra. People can be invited by email, or join by themselves with an email on the company's verified domain. A Workspace has no Trial: an Admin chooses the Workspace Plan on the Workspace's page before its first App ([`workspace_plan_needed`](https://basemodo.com/docs/errors#workspace_plan_needed)). Each person who joined the Workspace is a **seat**, Admins and Members alike; someone only invited is not one until they join, and someone taken out is not one any more. People you invite to a single App are not seats: they never pay, and neither does anyone else in the Workspace but the Workspace itself. ## What each Plan allows | Plan | Apps | Always on (workers) | Each App's Machine | Each App sends out | Logs kept | Backups kept | Failed cron runs retried | | --- | --- | --- | --- | --- | --- | --- | --- | | Trial | 1 | 0 | 256 MB, 1 CPU, 1 GB | 2 GB a day, 20 connections a second | 7 days | 7 days | none | | Starter | 1 | 0 | 256 MB, 1 CPU, 1 GB | 2 GB a day, 20 connections a second | 7 days | 7 days | none | | Pro | 10 | 3 | 512 MB, 1 CPU, 5 GB | 20 GB a day, 100 connections a second | 30 days | 30 days | 3, after 1, 2 and 4 minutes | | Workspace | 50 | 10 | 1024 MB, 2 CPUs, 5 GB | 50 GB a day, 200 connections a second | 30 days | 30 days | 3, after 1, 2 and 4 minutes | Apps and always-on Apps count per Owner; the Machine's memory, CPU and disk are each App's. An App with a [worker](https://basemodo.com/docs/workers-and-cron) never sleeps, so it is always on. ## Limits per App Each App's machine has a fixed share of memory, CPU and disk set by its Plan. An App that hits one is paused with the reason, and you are told by email and in your notifications. Upgrading applies at once. ## Abuse and limits Every App shares Basemodo's outgoing addresses with every other App, so each Plan also caps what an App sends out: a bandwidth per day and new connections per second. An App over a cap is slowed down until the day ends (UTC), never charged; `basemodo status` says so (`throttle`, with why and until when) and you are told in your notifications. An App flooded with requests is held to lower request limits for an hour, the same way. A new account is on **probation**: the Trial, and the first 14 days of a paid account or a Workspace. Probation has lower egress caps than any Plan, and no dedicated egress IP. Your billing page shows the caps you are held to and when probation ends. Basemodo watches for traffic that looks like abuse: sending far more than an App usually does, connecting to thousands of hosts (scanning), CPU busy for hours in an App with no worker (mining), looking up a mining pool or a command-and-control domain, hammering one host (alone or together with Apps of other accounts), or deploying the same files as an App already stopped for abuse. Such an App is paused at once with the reason, its outgoing traffic moved apart from everyone else's, and a person at Basemodo reviews it; you are told by email. Nothing deploys it meanwhile ([`app_under_review`](https://basemodo.com/docs/errors#app_under_review)). If nothing is wrong, it is resumed and you are told. Mining, a second App under review within 30 days, or any review during probation also suspends the account: every App paused and nothing new created or deployed ([`account_suspended`](https://basemodo.com/docs/errors#account_suspended)) until the review ends. Write to abuse@basemodo.com if you think a pause is a mistake. A Workspace App can leave from a **dedicated egress IP**, an outgoing address of its own to allowlist in your firewalls (once probation is over, where Basemodo's compute provider offers it). It is a paid extra, charged monthly per App on the Workspace's bill from the moment you take it, prorated, until you give it back: [`basemodo egress dedicated`](https://basemodo.com/docs/cli/egress) answers the address, `basemodo egress shared` gives it back (or the App's page, or `PUT /api/apps//egress {"dedicated": true}`). Deleting the App gives it back too. ## Logs, Backups and cron retries Each App keeps its [Logs](https://basemodo.com/docs/cli/logs) and each of its [Backups](https://basemodo.com/docs/data) as long as its Owner's Plan says, and on Pro and Workspace a [cron run](https://basemodo.com/docs/workers-and-cron) that fails or times out is tried again, each retry waiting twice as long as the one before; the Logs say when the next try is. [`basemodo status`](https://basemodo.com/docs/cli/status) shows what an App has. A change of Plan reaches every App at once. Upgrading keeps Logs longer from then on, and new Backups 30 days. Downgrading keeps Logs 7 days from then on, so older lines go; a Backup already taken keeps the expiry it was taken with, so none goes early. ## Paying Everything about money is on the billing page on basemodo.com: your Plan, your card and your invoices. - **Choosing a Plan without a card** (at the end of the Trial, or before) takes you to the payment provider's page to add one. Your Plan starts as soon as the card is added, and Apps paused by the end of the Trial resume where they were. - **Upgrading or downgrading** with a card on file applies at once. The difference for the rest of the month is charged now on an upgrade, and credited to your next invoices on a downgrade. A downgrade that your Apps do not fit in is refused before anything is charged ([`plan_too_small`](https://basemodo.com/docs/errors#plan_too_small)). - **Your card** is changed on the payment provider's page, from the billing page. If it is declined, nothing changes ([`card_declined`](https://basemodo.com/docs/errors#card_declined)). - **Invoices** arrive by email, once each, and are listed on the billing page. - **A Workspace's seats** are counted when each month starts: that month's invoice bills the seats it has then. Someone who joins mid-month is billed from the next month, and someone who leaves is not billed for the next. A Workspace's Plan, seats, card and invoices are on the Workspace's page, for its Admins; its invoices are emailed to every Admin. If the payment provider cannot charge your card after retrying, your Plan ends: your Apps pause with the reason, their data is kept 30 days, and choosing a Plan again resumes them ([`plan_ended`](https://basemodo.com/docs/errors#plan_ended)). --- # The basemodo CLI Every command of `basemodo`, the program that deploys and shares your Apps from a terminal, a script or an agent, with how it signs in and how it prints. ## Install and check `basemodo` is a single program with nothing else to install. Check it works: ```sh basemodo --version ``` Every command explains itself with `--help` (`basemodo deploy --help`), with examples; this page and the page of each command say the same, because they are written from the same source. ## Signing in ```sh basemodo login ``` Opens your browser to approve this computer, and keeps the sign-in for 30 days. Over SSH, or with `--device`, it shows a short code to enter at basemodo.com/device from any device instead. The sign-in is kept in `credentials.json`, readable only by you, in `$BASEMODO_CONFIG_DIR`, else `$XDG_CONFIG_HOME/basemodo`, else `~/.config/basemodo` (`%APPDATA%\basemodo` on Windows). The [local MCP](https://basemodo.com/docs/agents) uses the same sign-in. You can see and revoke it on the Devices page of basemodo.com. ## Which App Commands that act on an App take `--app NAME`. Without it they use the App named after the folder you are in, the way `basemodo deploy` names it (the folder's name, or `name` in [`basemodo.toml`](https://basemodo.com/docs/manifest#name)). So inside the App's folder you rarely type its name. ## Output, for people and for programs - Without `--json`, a command prints text for people on stdout, and messages along the way (progress, a build log) on stderr. - With `--json`, stdout holds exactly one JSON document, the result; each command's page says its shape. A command that takes a while also writes its progress to stderr as one JSON object per line: `{"event": "progress", "stage": "build", "message": "..."}`. - An error exits with a non-zero code. With `--json` it is `{"error": {"code", "message", "fix", "docs"}}` on stdout; without, the message on stderr with its `fix:` (the exact command or `basemodo.toml` line to change) and `docs:` (the page explaining it). Every code is explained on [the errors page](https://basemodo.com/docs/errors). - `basemodo run` exits with the exit code of the command it ran. Scripts and agents should always pass `--json`, and act on `error.code` and `error.fix`. ## Global options Every command takes these, before or after its own arguments. - `--json`: Print the result as JSON on stdout, for agents and scripts. - `--api-url `: The Basemodo API to talk to. Also read from `BASEMODO_API_URL`. Default: `https://api.basemodo.com`. ## Commands | Command | What it does | From an agent | | --- | --- | --- | | [`basemodo backup`](https://basemodo.com/docs/cli/backup) | Take a Backup of an App's data now: its database and every file on its volume | tool `backup` | | [`basemodo backups`](https://basemodo.com/docs/cli/backups) | List an App's Backups, newest first, to choose one to restore | tool `backups` | | [`basemodo db`](https://basemodo.com/docs/cli/db) | Run SQL against an App's SQLite database (/data/app.db) and print the rows | tool `db` | | [`basemodo deploy`](https://basemodo.com/docs/cli/deploy) | Deploy a folder and print its URL once it is live | tool `deploy` | | [`basemodo docs`](https://basemodo.com/docs/cli/docs) | Read the documentation of basemodo.com/docs: every page, or one topic | tool `docs` | | [`basemodo domains add`](https://basemodo.com/docs/cli/domains-add) | Claim a hostname of yours for the App, and print the DNS records to add | tool `domains_add` | | [`basemodo domains verify`](https://basemodo.com/docs/cli/domains-verify) | Look for a custom domain's DNS records now; once found, the App answers there | tool `domains_verify` | | [`basemodo domains list`](https://basemodo.com/docs/cli/domains-list) | List the App's custom domains, each with its status and DNS records | tool `domains_list` | | [`basemodo domains remove`](https://basemodo.com/docs/cli/domains-remove) | Stop the App answering at a custom domain, and forget its certificate | tool `domains_remove` | | [`basemodo egress`](https://basemodo.com/docs/cli/egress) | Give an App an outgoing address of its own (dedicated), give it back (shared), or say where its traffic leaves from | tool `egress` | | [`basemodo env set`](https://basemodo.com/docs/cli/env-set) | Set Secrets: NAME=value ..., or one NAME alone to read its value from stdin. The App gets them the next time it starts: basemodo restart | tool `env_set` | | [`basemodo env list`](https://basemodo.com/docs/cli/env-list) | List the App's Secrets by name, with who set each and when. Never values | tool `env_list` | | [`basemodo env unset`](https://basemodo.com/docs/cli/env-unset) | Remove Secrets by name (all or none). The App loses them the next time it starts: basemodo restart | tool `env_unset` | | [`basemodo indexing`](https://basemodo.com/docs/cli/indexing) | Let search engines index a Public App (on), keep them out (off), or say which | tool `indexing` | | [`basemodo logs`](https://basemodo.com/docs/cli/logs) | Follow an App's Logs live, or print its last lines with --lines | tool `logs` | | [`basemodo login`](https://basemodo.com/docs/cli/login) | Sign this CLI in from your browser | terminal only | | [`basemodo rename`](https://basemodo.com/docs/cli/rename) | Rename an App; its old address redirects to the new one for 30 days | tool `rename` | | [`basemodo mcp`](https://basemodo.com/docs/cli/mcp) | Serve the local MCP over stdio, for an agent on this machine | terminal only | | [`basemodo restore`](https://basemodo.com/docs/cli/restore) | Put an App's data back as a Backup holds it | tool `restore` | | [`basemodo open`](https://basemodo.com/docs/cli/open) | Open an App's URL in your browser | tool `open` | | [`basemodo restart`](https://basemodo.com/docs/cli/restart) | Restart an App: its Machine starts again on its live Deploy | tool `restart` | | [`basemodo rollback`](https://basemodo.com/docs/cli/rollback) | Make an earlier Deploy live again, as a new Deploy | tool `rollback` | | [`basemodo run`](https://basemodo.com/docs/cli/run) | Run a one-off command inside an App's Machine (a migration, a script) | tool `run` | | [`basemodo share`](https://basemodo.com/docs/cli/share) | Share an App: invite Members and Editors by email, remove them, answer Access Requests, or change its Visibility | tool `share` | | [`basemodo status`](https://basemodo.com/docs/cli/status) | Say whether an App is awake, asleep, starting or paused, and its latest Deploy | tool `status` | | [`basemodo toolbar`](https://basemodo.com/docs/cli/toolbar) | Turn off the Toolbar on an App's pages (off), turn it back on (on), or say which | tool `toolbar` | | [`basemodo version`](https://basemodo.com/docs/cli/version) | Print the version of this CLI | terminal only | | [`basemodo whoami`](https://basemodo.com/docs/cli/whoami) | Print who this CLI is signed in as | tool `whoami` | --- # basemodo backup Take a Backup of an App's data now: its database and every file on its volume. ```sh basemodo backup [OPTIONS] ``` Basemodo already takes one a day; take one before something risky (a migration, a bulk edit), then `basemodo restore ` if it goes wrong. Each Backup is kept 7 days. The App keeps running meanwhile. ## Arguments - `--app `: The App to back up. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Back up the App of this folder before a migration: ```sh basemodo backup basemodo run "npm run migrate" ``` Back up another App, keeping the id for a script: ```sh basemodo backup --app lunch-rota --json ``` ## Output With `--json`, it prints {"id", "app", "kind" (manual), "size_bytes", "created_at", "expires_at"}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `backup`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `app` (text). It answers what the command prints with `--json`. ## Explained in - [Data: the disk, SQLite, db, run and Backups](https://basemodo.com/docs/data) --- # basemodo backups List an App's Backups, newest first, to choose one to restore. ```sh basemodo backups [OPTIONS] ``` Basemodo's daily ones (kind daily), those taken with `basemodo backup` (manual), and a deleted App's last one (final), each until it expires. ## Arguments - `--app `: The App whose Backups to list. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples List the Backups of the App of this folder: ```sh basemodo backups ``` List another App's: ```sh basemodo backups --app lunch-rota ``` ## Output With `--json`, it prints {"backups": [{"id", "app", "kind", "size_bytes", "created_at", "expires_at"}], "retention_days"}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `backups`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `app` (text). It answers what the command prints with `--json`. --- # basemodo db Run SQL against an App's SQLite database (/data/app.db) and print the rows. ```sh basemodo db [OPTIONS] [SQL] ``` Every App has a SQLite file at /data/app.db on its volume ($BASEMODO_DB in its processes), made by whatever opens it first. Several statements run in order and stop at the first error; what ran before it stays done. The rows are those of the last statement that returned any. A sleeping App is woken to run it. In a terminal the SQL may come on stdin. ## Arguments - `SQL`: The SQL to run; `-` or nothing reads it from stdin. - `--app `: The App whose database to use. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Look at the latest rows of a table: ```sh basemodo db "select * from tasks order by id desc limit 10" ``` List the tables: ```sh basemodo db "select name from sqlite_master where type = 'table'" ``` Run a file of SQL against another App: ```sh basemodo db --app lunch-rota < fix.sql ``` ## Output With `--json`, it prints {"columns": [...], "rows": [[...], ...]}, empty for statements that return no rows. SQL that fails is an error with code sql_error and SQLite's message. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `db`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `sql` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Data: the disk, SQLite, db, run and Backups](https://basemodo.com/docs/data) --- # basemodo deploy Deploy a folder and print its URL once it is live. ```sh basemodo deploy [OPTIONS] [PATH] ``` Packs the folder (leaving out .git and what .gitignore, .ignore, .basemodoignore and .dockerignore exclude), uploads it, and Basemodo detects how to build and run it (or reads basemodo.toml), builds it and starts it; the command waits until the App answers HTTP at its URL. The App is created the first time. A Deploy that fails leaves the previous one live. With --repository there is no folder: the App is linked to that GitHub repository and its branch's latest commit is deployed. ## Arguments - `PATH`: The folder to deploy. Default: the current one. - `--name `: The App to deploy to. Default: `name` in basemodo.toml, else the folder's name (with --repository, the repository's name). - `--repository `: A GitHub repository to deploy instead of a folder, as owner/repo: the App is linked to it and its branch's latest commit is deployed now, and again on every push. Basemodo's GitHub App must be able to read it. - `--branch `: With --repository: the branch to deploy. Default: the one the App is linked to, else the repository's default branch. - `--subdir `: With --repository: the folder of the repository that holds the App (apps/web). Default: the one the App is linked to, else the whole repository. - `--workspace `: The Workspace whose App this is, by name: deploy to its App of that name, creating it the first time (which takes an Admin of the Workspace). Default: your own App of that name, else the one App of that name a Workspace of yours owns, else a new App of your own. - `--link`: In a clone of a GitHub repository: link the App to it (this branch and folder) without asking, so every push deploys it. - `--no-link`: Do not offer to link the App to the folder's GitHub repository. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Deploy the folder you are in: ```sh basemodo deploy ``` Deploy another folder, as an App of another name: ```sh basemodo deploy ./site --name team-lunch ``` Deploy and keep the result for a script: ```sh basemodo deploy --json > deploy.json ``` Deploy a GitHub repository's latest commit, without a clone (pushes deploy too): ```sh basemodo deploy --repository ana/lunch-rota ``` ## Output With `--json`, it prints {"url", "app", "deploy"}, the live Deploy with what was detected; progress events on stderr (upload, detect, queued, build, start, ready). A failed Deploy is an error with "deploy" and the build "log". ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `deploy`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `path` (text), `name` (text), `repository` (text), `branch` (text), `subdir` (text), `workspace` (text), `link` (flag), `no_link` (flag). It answers what the command prints with `--json`. ## Explained in - [Deploying](https://basemodo.com/docs/deploying) - [Deploying from GitHub](https://basemodo.com/docs/github) --- # basemodo docs Read the documentation of basemodo.com/docs: every page, or one topic. ```sh basemodo docs [OPTIONS] [TOPIC]... ``` The same text as the site and https://basemodo.com/llms.txt, read from this CLI, so it works offline and matches this version. A topic is a command (deploy, env set), a page as --list names it (manifest, sharing), or a page's title; a group of commands (env) gives each of its pages. ## Arguments - `TOPIC...`: What to read: a command (deploy, env set), a page as --list names it (manifest, sharing, cli/env-set), or a page's title. Without one, every page. - `--list`: List the pages, with their addresses, instead of printing them. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Read everything (pipe it to a pager, or give it to an agent): ```sh basemodo docs | less ``` Read the page of a command: ```sh basemodo docs env set ``` Read about basemodo.toml: ```sh basemodo docs manifest ``` List the pages: ```sh basemodo docs --list ``` ## Output With `--json`, it prints {"pages": [{"slug", "title", "description", "section", "url", "markdown"}]}, without markdown for --list. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `docs`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `topic` (list of text), `list` (flag). It answers what the command prints with `--json`. --- # basemodo domains add Claim a hostname of yours for the App, and print the DNS records to add. ```sh basemodo domains add [OPTIONS] ``` Add both at the domain's DNS host: the CNAME points the hostname at Basemodo, and the TXT proves it is yours (only its value lets this App claim it). Then verify. Custom domains are on the Pro and Workspace Plans. ## Arguments - `HOSTNAME` (required): The hostname, like app.example.com (a subdomain: the CNAME cannot sit at a domain's apex). - `--app `: The App whose custom domains to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Give the App of this folder its own address: ```sh basemodo domains add app.example.com ``` The same for another App: ```sh basemodo domains add rota.example.com --app lunch-rota ``` ## Output With `--json`, it prints {"hostname", "url", "status" (pending, issuing, active or failed), "records": [{"type", "name", "value"}], "problem", "created_at", "verified_at", "certificate_expires_at"}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `domains_add`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `hostname` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Domains](https://basemodo.com/docs/domains) --- # basemodo domains verify Look for a custom domain's DNS records now; once found, the App answers there. ```sh basemodo domains verify [OPTIONS] ``` Basemodo also looks by itself every few minutes for a week. Verified, the App answers at the hostname at once, and over HTTPS once its certificate is issued (status active); the error says which record is missing otherwise. ## Arguments - `HOSTNAME` (required): The hostname, as added. - `--app `: The App whose custom domains to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Check the records you added: ```sh basemodo domains verify app.example.com ``` ## Output With `--json`, it prints {"hostname", "url", "status" (pending, issuing, active or failed), "records": [{"type", "name", "value"}], "problem", "created_at", "verified_at", "certificate_expires_at"}. Records not found yet are an error with code domain_not_verified naming the missing one. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `domains_verify`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `hostname` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Domains](https://basemodo.com/docs/domains) --- # basemodo domains list List the App's custom domains, each with its status and DNS records. ```sh basemodo domains list [OPTIONS] ``` ## Arguments - `--app `: The App whose custom domains to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples List the custom domains of the App of this folder: ```sh basemodo domains list ``` The same for another App: ```sh basemodo domains list --app lunch-rota ``` ## Output With `--json`, it prints {"app", "domains": [{"hostname", "url", "status", "records", "problem", "created_at", "verified_at", "certificate_expires_at"}]}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `domains_list`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `app` (text). It answers what the command prints with `--json`. ## Explained in - [Domains](https://basemodo.com/docs/domains) --- # basemodo domains remove Stop the App answering at a custom domain, and forget its certificate. ```sh basemodo domains remove [OPTIONS] ``` ## Arguments - `HOSTNAME` (required): The hostname, as added. - `--app `: The App whose custom domains to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Remove a custom domain: ```sh basemodo domains remove app.example.com ``` ## Output With `--json`, it prints {"app", "domains": [{"hostname", "url", "status", "records", "problem", "created_at", "verified_at", "certificate_expires_at"}]}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `domains_remove`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `hostname` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Domains](https://basemodo.com/docs/domains) --- # basemodo egress Give an App an outgoing address of its own (dedicated), give it back (shared), or say where its traffic leaves from. ```sh basemodo egress [OPTIONS] [dedicated|shared] ``` By default an App's outbound traffic leaves from Basemodo's shared addresses. A Dedicated Egress IP is an address of the App's own, for a partner's firewall to allowlist: a paid extra of a Workspace App, charged monthly on the Workspace's bill from now on, after its first 14 days. Shared gives it back and the charge stops. Only the App's Owner changes it. ## Arguments - `dedicated|shared`: dedicated gives the App an outgoing address of its own (a paid extra of a Workspace App); shared gives it back. Without it, only show where its traffic leaves from. - `--app `: The App. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Give the App of this folder an address of its own, and print it: ```sh basemodo egress dedicated ``` Give it back to the shared pool: ```sh basemodo egress shared --app crm ``` Say where an App's traffic leaves from: ```sh basemodo egress --app crm --json ``` ## Output With `--json`, it prints {"app", "pool" (shared, quarantine or dedicated), "address": {"v4", "v6"} (only while dedicated)}. A personal App is an error with code dedicated_ip_not_on_plan; a declined card, card_declined. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `egress`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `pool` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Plans](https://basemodo.com/docs/plans) --- # basemodo env set Set Secrets: NAME=value ..., or one NAME alone to read its value from stdin. The App gets them the next time it starts: basemodo restart. ```sh basemodo env set [OPTIONS] ... ``` All are set or none. Names are letters, digits and _, not starting with a digit; PORT and BASEMODO_* are Basemodo's. ## Arguments - `NAME=value...` (required): The Secrets, each as NAME=value (everything after the first = is the value). In a terminal, one NAME alone reads its value from stdin. - `--app `: The App whose Secrets to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Set two Secrets: ```sh basemodo env set STRIPE_KEY=sk_live_123 MAILER_TOKEN=abc ``` Set one from a file, so the value stays out of your shell history: ```sh basemodo env set STRIPE_KEY < stripe-key.txt ``` ## Output With `--json`, it prints {"secrets": [{"name", "updated_by", "updated_at"}], "changes": [{"name", "action", "by", "at"}]}, never a value. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `env_set`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `pairs` (list of text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Secrets](https://basemodo.com/docs/secrets) --- # basemodo env list List the App's Secrets by name, with who set each and when. Never values. ```sh basemodo env list [OPTIONS] ``` ## Arguments - `--app `: The App whose Secrets to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples List the Secrets of the App of this folder: ```sh basemodo env list ``` The same for another App: ```sh basemodo env list --app lunch-rota ``` ## Output With `--json`, it prints {"secrets": [{"name", "updated_by", "updated_at"}], "changes": [{"name", "action", "by", "at"}]}, never a value. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `env_list`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `app` (text). It answers what the command prints with `--json`. ## Explained in - [Secrets](https://basemodo.com/docs/secrets) --- # basemodo env unset Remove Secrets by name (all or none). The App loses them the next time it starts: basemodo restart. ```sh basemodo env unset [OPTIONS] ... ``` ## Arguments - `NAME...` (required): The names of the Secrets to remove. - `--app `: The App whose Secrets to change. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Remove a Secret: ```sh basemodo env unset MAILER_TOKEN ``` ## Output With `--json`, it prints {"secrets": [{"name", "updated_by", "updated_at"}], "changes": [{"name", "action", "by", "at"}]}, never a value. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `env_unset`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `names` (list of text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Secrets](https://basemodo.com/docs/secrets) --- # basemodo indexing Let search engines index a Public App (on), keep them out (off), or say which. ```sh basemodo indexing [OPTIONS] [on|off] ``` No App is indexed unless its Owner says so: every answer says noindex and robots.txt disallows everything. On (or [web] indexable = true in basemodo.toml) lets search engines into a Public App, for a landing page that should be found; it is refused on a Private or Link App. Making the App anything but Public turns indexing off again. ## Arguments - `on|off`: on lets search engines index the App (a Public App only), off keeps them out. Without it, only show whether they may. - `--app `: The App. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Let search engines find a Public App: ```sh basemodo indexing on ``` Keep them out again: ```sh basemodo indexing off ``` Say whether an App is indexable: ```sh basemodo indexing --app lunch-rota ``` ## Output With `--json`, it prints {"app", "url", "visibility", "indexable"}, after the change. On an App that is not Public, on is an error with code app_not_public. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `indexing`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `turn` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Sharing](https://basemodo.com/docs/sharing) --- # basemodo logs Follow an App's Logs live, or print its last lines with --lines. ```sh basemodo logs [OPTIONS] ``` Logs are what the App's processes print and what its builds print, kept 7 days. Without --lines or --after the CLI follows them until interrupted; an agent gets the latest 100 lines and a cursor, and follows by asking again with --after. ## Arguments - `--app `: The App whose Logs to read. Default: the one named after the current folder. - `--lines `: Print the last N lines and stop, instead of following. - `--after `: Print the lines after this cursor (from an earlier `--json` answer) and stop. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Follow the Logs of the App of this folder (Ctrl-C to stop): ```sh basemodo logs ``` Print the last 50 lines and stop: ```sh basemodo logs --lines 50 ``` Print what is new since an earlier answer: ```sh basemodo logs --after --json ``` ## Output With `--json`, it prints {"lines": [{"at", "source", "deploy", "message"}], "cursor", "retention_days"}; while following, one JSON line per log line. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `logs`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `app` (text), `lines` (number), `after` (text). It answers what the command prints with `--json`. ## Explained in - [Getting started](https://basemodo.com/docs/getting-started) - [Deploying](https://basemodo.com/docs/deploying) - [Workers and cron](https://basemodo.com/docs/workers-and-cron) - [Deploying from GitHub](https://basemodo.com/docs/github) - [How Apps run](https://basemodo.com/docs/how-apps-run) - [Plans](https://basemodo.com/docs/plans) --- # basemodo login Sign this CLI in from your browser. ```sh basemodo login [OPTIONS] ``` Opens Basemodo in the browser to approve this device (or, over SSH, shows a code to enter at basemodo.com/device) and keeps the token. The local MCP uses the same login. ## Arguments - `--device`: Show a code to enter in a browser on any device, instead of opening the browser here. Chosen by itself over SSH. - `--no-browser`: Print the sign-in URL instead of opening the browser. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Sign in from the browser of this computer: ```sh basemodo login ``` Sign in over SSH, or from another device, with a code: ```sh basemodo login --device ``` ## Output With `--json`, it prints {"person": {"id", "email"}}. ## From an agent Terminal only: it needs the person at their browser; an agent asks them to run it. ## Explained in - [Getting started](https://basemodo.com/docs/getting-started) --- # basemodo rename Rename an App; its old address redirects to the new one for 30 days. ```sh basemodo rename [OPTIONS] ``` Its address (slug) follows the new name, with a short suffix if another App has it. Links to the old address keep working for 30 days, path and all, and no other App can take it meanwhile. Custom domains are not affected. Deploy it from then on with --name and the new name (or from a folder of that name). Only the App's Owner renames it. ## Arguments - `NAME` (required): The new name: lowercase letters, digits and dashes, at most 40. - `--app `: The App to rename. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Rename the App of this folder: ```sh basemodo rename team-lunch ``` Rename another App: ```sh basemodo rename team-lunch --app lunch-rota ``` ## Output With `--json`, it prints {"app", "previous_url", "redirects_until"}; both null when the address did not change. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `rename`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `name` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Domains](https://basemodo.com/docs/domains) --- # basemodo mcp Serve the local MCP over stdio, for an agent on this machine. ```sh basemodo mcp [OPTIONS] ``` Its tools are this CLI's commands, signed in with this CLI's login. Add it to Claude Code with `claude mcp add --scope user basemodo -- basemodo mcp`, to Codex with `codex mcp add basemodo -- basemodo mcp`, and to Cursor in ~/.cursor/mcp.json: {"mcpServers": {"basemodo": {"command": "basemodo", "args": ["mcp"]}}}. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Add Basemodo to Claude Code: ```sh claude mcp add --scope user basemodo -- basemodo mcp ``` Add Basemodo to Codex: ```sh codex mcp add basemodo -- basemodo mcp ``` ## From an agent Terminal only: it is the MCP. --- # basemodo restore Put an App's data back as a Backup holds it. ```sh basemodo restore [OPTIONS] ``` Its volume (the database and every file under /data) becomes what it was when the Backup was taken: everything written since is gone, so take a Backup first if it may be needed. The Backup stays, and the App runs again afterwards. Restoring a deleted App's last Backup brings the App back. ## Arguments - `BACKUP` (required): The Backup to restore: its id, as `basemodo backups` lists it or `basemodo backup --json` prints it. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Find the Backup to go back to, then restore it: ```sh basemodo backups basemodo restore 0b6f3c1e-6f3a-4d55-9d43-5b8a1f0e2c11 ``` ## Output With `--json`, it prints {"app", "backup"}. A Backup that does not exist or expired is an error with code backup_not_found. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `restore`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `backup` (text). It answers what the command prints with `--json`. ## Explained in - [Data: the disk, SQLite, db, run and Backups](https://basemodo.com/docs/data) --- # basemodo open Open an App's URL in your browser. ```sh basemodo open [OPTIONS] ``` The App of the current folder, or `--app`. The browser signs in to the App on the way, with your sign-in on basemodo.com, so you arrive as yourself even at a Public App. With `--json` (and for an agent) nothing is opened: it prints the App's URL, to hand on. A Private App asks whoever opens it to sign in. ## Arguments - `--app `: The App to open. Default: the one named after the current folder. - `--no-browser`: Print the URL without opening a browser. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Open the App of this folder: ```sh basemodo open ``` Print another App's URL for a script: ```sh basemodo open --app lunch-rota --json ``` ## Output With `--json`, it prints {"app", "url", "opened" (false: nothing is opened with --json)}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `open`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `app` (text), `no_browser` (flag). It answers what the command prints with `--json`. ## Explained in - [Getting started](https://basemodo.com/docs/getting-started) - [Sharing](https://basemodo.com/docs/sharing) --- # basemodo restart Restart an App: its Machine starts again on its live Deploy. ```sh basemodo restart [OPTIONS] ``` It starts with the App's Secrets as they are now, so a Secret set with `basemodo env set` applies without a new Deploy (waking from sleep does the same). A cron run going on stops. An App paused because a Process kept crashing or ran out of its memory or disk quota is resumed; one paused until its Owner chooses a Plan, or for review, stays paused. Owners and Editors restart an App. ## Arguments - `--app `: The App to restart. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Apply a Secret just set to the App of this folder: ```sh basemodo env set STRIPE_KEY=sk_live_123 basemodo restart ``` Resume an App paused after its worker kept crashing, once fixed: ```sh basemodo restart --app newsletter ``` ## Output With `--json`, it prints the App's status after, as status prints it: {"app", "state", "latest_deploy", "public_paths", "extras"}. An App with no live Deploy is an error with code app_not_deployed. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `restart`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `app` (text). It answers what the command prints with `--json`. ## Explained in - [Getting started](https://basemodo.com/docs/getting-started) - [Secrets](https://basemodo.com/docs/secrets) - [How Apps run](https://basemodo.com/docs/how-apps-run) --- # basemodo rollback Make an earlier Deploy live again, as a new Deploy. ```sh basemodo rollback [OPTIONS] ``` Nothing is built: the new Deploy runs the earlier one's image, with the App's Secrets as they are now, and waits until it answers HTTP. ## Arguments - `DEPLOY` (required): The Deploy to make live again: its id, as `basemodo deploy --json` prints it (`deploy.id`). It must have gone live once. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Make an earlier Deploy live again (its id from deploy --json or the App's page): ```sh basemodo rollback 0b8f6a52-3f0e-4c1e-9a57-5d1c2e7f9a10 ``` ## Output With `--json`, it prints {"url", "app", "deploy"}, as deploy prints it; progress events on stderr. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `rollback`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `deploy` (text). It answers what the command prints with `--json`. ## Explained in - [Getting started](https://basemodo.com/docs/getting-started) - [Deploying](https://basemodo.com/docs/deploying) --- # basemodo run Run a one-off command inside an App's Machine (a migration, a script). ```sh basemodo run [OPTIONS] ... ``` It runs where the App's data is: with its volume at /data (its database at /data/app.db) and the environment its processes started with at its latest Deploy, Secrets included. One argument is a shell line run with sh -c ("npm run migrate && echo done"); several are quoted into one. Its output is printed as it comes, and the CLI exits with its exit code (124 when it ran past --timeout). A sleeping App is woken to run it. ## Arguments - `COMMAND...` (required): The command to run in the App's Machine. - `--app `: The App to run it in. Default: the one named after the current folder. - `--timeout `: How long it may run, in seconds (at most 900). Default: `60`. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Run a migration: ```sh basemodo run "npm run migrate" ``` Look at the App's disk (several arguments after -- are one command): ```sh basemodo run -- ls -la /data ``` Run a long import, for up to 10 minutes: ```sh basemodo run --timeout 600 "python import.py" ``` ## Output With `--json`, it prints {"command", "exit_code" (null when it timed out), "timed_out", "stdout", "stderr"}; while it runs, {"event": "output", "stream", "text"} lines on stderr. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `run`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `command` (list of text), `app` (text), `timeout` (number). It answers what the command prints with `--json`. ## Explained in - [Data: the disk, SQLite, db, run and Backups](https://basemodo.com/docs/data) - [How Apps run](https://basemodo.com/docs/how-apps-run) --- # basemodo share Share an App: invite Members and Editors by email, remove them, answer Access Requests, or change its Visibility. ```sh basemodo share [OPTIONS] [EMAIL]... ``` Each invited email gets the App's URL and becomes a Member the first time they open it signed in. An Editor (`--editor`) is a Member who may also deploy the App, read its logs and change its Secrets from their own account, but not change who can reach it. Someone refused at a Private App can ask for access; `--approve` makes them a Member, `--decline` says no. Private lets in only the Owner, Members and Editors, Link anyone with the URL who signs in, Public anyone. Only the Owner shares an App. With no arguments it only shows who can reach the App. ## Arguments - `EMAIL...`: Emails to invite as Members. - `--editor ` (repeatable): Add an Editor: a Member who may also deploy the App, read its logs and change its Secrets, but not change who can reach it. Repeat for several. Someone already invited becomes an Editor. - `--visibility `: Who can reach the App: private (its Owner and Members), workspace (also everyone in the Workspace that owns it), link (anyone with its URL who signs in) or public (anyone, no sign-in). - `--remove ` (repeatable): Remove a Member or an Editor, or take back an invitation. Repeat for several. - `--approve ` (repeatable): Approve the Access Request of this email: they become a Member. Repeat for several. - `--decline ` (repeatable): Decline the Access Request of this email. Repeat for several. - `--app `: The App to share. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Invite two people: ```sh basemodo share ana@example.com ben@example.com ``` Let the developer helping you deploy, read Logs and change Secrets: ```sh basemodo share --editor dev@example.com ``` Let in anyone with the link who signs in: ```sh basemodo share --visibility link ``` Remove someone: ```sh basemodo share --remove ben@example.com ``` Approve someone who asked for access: ```sh basemodo share --approve carla@example.com ``` See who can reach the App, and who asked: ```sh basemodo share ``` ## Output With `--json`, it prints {"app", "url", "visibility", "members": [{"id", "email", "role" (member or editor), "status" (invited or accepted), "invited_at", "accepted_at"}], "access_requests": [{"id", "email", "requested_at"}]}, after the changes. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `share`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `invite` (list of text), `editor` (list of text), `visibility` (text), `remove` (list of text), `approve` (list of text), `decline` (list of text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Sharing](https://basemodo.com/docs/sharing) --- # basemodo status Say whether an App is awake, asleep, starting or paused, and its latest Deploy. ```sh basemodo status [OPTIONS] ``` An App nobody visits for 10 minutes falls asleep (unless it runs a worker); the next visit wakes it in a few seconds, never with an error. An App whose worker crash-loops, or that runs out of its memory or disk quota, is paused until it is deployed again; one whose Owner's Trial ended, until they choose a Plan. It also says what the Owner's Plan gives the App: how long its Logs and Backups are kept, and how many times a failed cron run is tried again; and what went wrong in the last day: failed Deploys, Processes that crashed or ran out of memory or disk, failed cron runs. ## Arguments - `--app `: The App to look at. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Is the App of this folder up?: ```sh basemodo status ``` The same for another App, for a script: ```sh basemodo status --app lunch-rota --json ``` ## Output With `--json`, it prints {"app", "state" (awake, asleep, starting, paused, or null before the first Deploy), "paused_reason" and "resume" (what resumes it; both only while paused), "latest_deploy", "public_paths", "extras": {"log_retention_days", "backup_retention_days", "cron_retries"}, "recent_errors": [{"at", "kind" (deploy_failed, crashed, crash_loop, out_of_memory, disk_full, cron_failed), "message", "deploy"}]}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `status`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: `app` (text). It answers what the command prints with `--json`. ## Explained in - [Getting started](https://basemodo.com/docs/getting-started) - [Public Paths: webhooks](https://basemodo.com/docs/public-paths) - [How Apps run](https://basemodo.com/docs/how-apps-run) - [Plans](https://basemodo.com/docs/plans) --- # basemodo toolbar Turn off the Toolbar on an App's pages (off), turn it back on (on), or say which. ```sh basemodo toolbar [OPTIONS] [on|off] ``` Basemodo shows its Toolbar, a small pill, on an App's pages to signed-in visitors: who is here now, the App's Visibility and a way to share it. It is on for every App; off (or [web] toolbar = false in basemodo.toml) stops showing it, for an App whose own pages should be all there is. Only the Owner changes it. ## Arguments - `on|off`: on shows the Toolbar on the App's pages to signed-in visitors, off stops showing it. Without it, only show whether it shows. - `--app `: The App. Default: the one named after the current folder. Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Stop showing the Toolbar on the App of this folder: ```sh basemodo toolbar off ``` Show it again: ```sh basemodo toolbar on ``` Say whether an App shows it: ```sh basemodo toolbar --app lunch-rota ``` ## Output With `--json`, it prints {"app", "url", "toolbar"}, after the change. An Editor is refused with code owner_only. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `toolbar`. It changes things, so an agent's client may ask you before it runs. Its arguments are the command's, named as JSON keys: `turn` (text), `app` (text). It answers what the command prints with `--json`. ## Explained in - [Sharing](https://basemodo.com/docs/sharing) --- # basemodo version Print the version of this CLI. ```sh basemodo version [OPTIONS] ``` Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Print the version: ```sh basemodo version ``` ## Output With `--json`, it prints {"version"}. ## From an agent Terminal only: the MCP names its version when the agent connects. --- # basemodo whoami Print who this CLI is signed in as. ```sh basemodo whoami [OPTIONS] ``` Like every command, it also takes the [global options](https://basemodo.com/docs/cli#global-options) `--json` and `--api-url`. ## Examples Check which account this CLI uses: ```sh basemodo whoami ``` ## Output With `--json`, it prints {"id", "email"}. ## From an agent The [local MCP](https://basemodo.com/docs/agents) serves it as the tool `whoami`. It only reads, so an agent's client may run it without asking you. Its arguments are the command's, named as JSON keys: it takes none. It answers what the command prints with `--json`. --- # The manifest: basemodo.toml Every key of `basemodo.toml`, the optional file that names your App and says what detection cannot know: its start command, runtime versions, workers, cron entries and Public Paths. Most Apps need no manifest: Basemodo detects how to build and run the folder (see [Supported runtimes](https://basemodo.com/docs/runtimes)). Add a `basemodo.toml` at the top of the folder only for the exceptions. It is [TOML](https://toml.io), and every key is optional. ```toml name = "lunch-rota" [web] command = "node server.js" ``` - **Strict.** A key Basemodo does not know, or a wrong value, stops the Deploy before anything is built, with the error [`invalid_manifest`](https://basemodo.com/docs/errors#invalid_manifest) naming the key, its line, and the fix. A typo never goes unnoticed. - **On top of detection.** What the manifest changes in the detected plan is listed as an override, with its line, next to the detection's explanation, so you always see why your App is built the way it is. - **With your own Dockerfile**, `web.command` replaces the image's command, and `[build]` versions are refused: the Dockerfile's `FROM` line chooses the runtime. ## Every key at once No App needs all of these; this is every key `basemodo.toml` accepts, each explained below. ```toml name = "lunch-rota" public_paths = ["/webhooks/stripe", "/hooks/*"] [web] command = "node server.js" indexable = true toolbar = false [build] node = "22" python = "3.12" [[worker]] name = "mailer" command = "node mailer.js" [[cron]] name = "nightly-report" schedule = "0 3 * * *" command = "node report.js" timezone = "America/Bogota" timeout = "15m" [network] allow = ["api.stripe.com", "*.trello.com"] ``` ## Keys ### `name` The App's name: `basemodo deploy` deploys to the App of this name. ```toml name = "lunch-rota" ``` ### `public_paths` Paths that bypass sign-in, for webhooks, under stricter rate and size limits; `/*` at the end opens every path under it. ```toml public_paths = ["/webhooks/stripe", "/hooks/*"] ``` ### `web` The web Process, which answers HTTP. ```toml [web] ``` ### `web.command` Starts the web Process instead of the detected start command; it must listen on $PORT. ```toml [web] command = "node server.js" ``` ### `web.indexable` Lets search engines index the App; only while its Visibility is Public. ```toml [web] indexable = true ``` ### `web.toolbar` Turns off the Toolbar Basemodo shows signed-in visitors on the App's pages (`true` turns it back on); without it, the App keeps the setting it has. ```toml [web] toolbar = false ``` ### `build` Runtime versions, one key per runtime. ```toml [build] ``` ### `build.node` The Node.js version, instead of the detected one. ```toml [build] node = "22" ``` ### `build.python` The Python version, instead of the detected one. ```toml [build] python = "3.12" ``` ### `worker` A Process that runs continuously beside web, in the same Machine; repeatable. ```toml [[worker]] ``` ### `worker.name` The worker's name, in logs. ```toml [[worker]] name = "mailer" ``` ### `worker.command` How the worker starts. ```toml [[worker]] command = "node mailer.js" ``` ### `cron` A Process run on a schedule; repeatable. ```toml [[cron]] ``` ### `cron.name` The entry's name, in logs. ```toml [[cron]] name = "nightly-report" ``` ### `cron.schedule` Five fields: minute hour day-of-month month day-of-week. ```toml [[cron]] schedule = "0 3 * * *" ``` ### `cron.command` What runs. ```toml [[cron]] command = "node report.js" ``` ### `cron.timezone` The time zone the schedule is read in; default UTC. ```toml [[cron]] timezone = "America/Bogota" ``` ### `cron.timeout` How long a run may take before it is killed; default 15m, at most 24h. ```toml [[cron]] timeout = "15m" ``` ### `network` Outbound network rules (Workspace Apps only). ```toml [network] ``` ### `network.allow` The only hosts the App may reach (`*.` for every subdomain); what Basemodo always blocks stays blocked. ```toml [network] allow = ["api.stripe.com", "*.trello.com"] ``` --- # Supported runtimes What Basemodo recognizes in a folder with no configuration, how it builds and starts each, and what to do when your App is something else. Basemodo reads your folder's files (never the network) and says what it found and why: the files and fields it looked at. The most specific match wins: your own `Dockerfile` first, then a framework (Next.js before plain Node, Django before plain Python), then a runtime, then a static website. - **Versions** come from your files (`.nvmrc`, `engines`, `.python-version`, `requires-python`); pin another with [`[build]`](https://basemodo.com/docs/manifest#build). - **The start command** comes from your framework or your scripts; give another with [`web.command`](https://basemodo.com/docs/manifest#web.command). Whatever starts, it must listen on `$PORT` (see [How Apps run](https://basemodo.com/docs/how-apps-run#the-port-contract)). - **Anything else** deploys with a `Dockerfile` at the top of the folder. When nothing matches, the Deploy stops with [`nothing_detected`](https://basemodo.com/docs/errors#nothing_detected) and the fix names the file to add. ## What is detected | Runtime | Detected by | Plan | | --- | --- | --- | | Own Dockerfile (first: it wins over every detector) | `Dockerfile` at the top of the folder; `EXPOSE` in its last stage only to warn when it is not `$PORT` | built from that Dockerfile as it is (`web.command` becomes its entrypoint), run with `PORT=8080`: the web process must listen on `$PORT` (the Port Contract; a container that never answers fails `not_ready` naming it) | | Static site | `index.html` at the top of the folder, and no build signal | the whole folder served by Caddy on port 8080, a missing path answered with the folder's `404.html` when it has one; no install, build or start command | | Node: package manager and version (every Node row) | `packageManager` in `package.json`, else the lockfile: `pnpm-lock.yaml` (pnpm), `yarn.lock` (yarn; Yarn 2+ with `.yarnrc.yml`), `bun.lock`/`bun.lockb` (bun), `package-lock.json` (npm), none (npm). Version: `.nvmrc` (`22`, `v22.12.0`, `lts/jod`), else `.node-version`, else `engines.node` (the major the range allows, 24 first, then 22, 20, 26, 18) | install `npm ci` / `npm install` / `pnpm install --frozen-lockfile` / `yarn install --frozen-lockfile` (`--immutable`) / `bun install --frozen-lockfile`; image `node:-slim`, `node:24-slim` when nothing pins one; pnpm and yarn through Corepack, bun from npm | | Next.js | `next` in `dependencies` or `devDependencies` of `package.json` | `scripts.build` (else `next build`), `scripts.start` (else `next start`), port 3000 | | Astro | `astro` in the dependencies; `@astrojs/node` makes it a server | `scripts.build` (else `astro build`); with `@astrojs/node`: `HOST=0.0.0.0 node ./dist/server/entry.mjs` on port 3000; without: `dist/` served by Caddy on port 8080 (`scripts.start`, `astro dev` in Astro's templates, is ignored) | | Vite | `vite` in the dependencies and no `scripts.start` | `scripts.build` (else `vite build`), then `dist/` served by Caddy on port 8080 | | Express | `express` in the dependencies | `scripts.build` if any; `scripts.start`, else `node
`, else `node server.js` (`index.js`, `app.js`, `main.js`); port 3000 | | Hono | `hono` in the dependencies (Node with `@hono/node-server`, or Bun) | as Express | | Node | `package.json` with `scripts.start`, `main`, or a `server.js` (`index.js`, `app.js`, `main.js`) at the top | as Express | | Python (any) | `uv.lock` → uv; `poetry.lock` or `[tool.poetry]` → poetry; `requirements.txt` (and its `-r` includes) → pip; a `pyproject.toml` `[project]` alone → uv. Version: `.python-version`, else `requires-python` (or poetry's `python`), which picks 3.13 if allowed, else the newest of 3.9 to 3.14 allowed; else 3.13 | `python:-slim`, dependencies in `/opt/venv` with `uv sync --locked --no-dev`, `poetry install --only main --no-root` or `pip install -r requirements.txt`; port 8000. Only main dependencies count. A Python folder no framework below starts is detected with no start command, and the Deploy asks for `[web] command` | | Django | the `django` dependency, `manage.py` naming `DJANGO_SETTINGS_MODULE`, and `/wsgi.py` (or `asgi.py` with uvicorn) | `gunicorn .wsgi`, or `uvicorn .asgi:application` when uvicorn and not gunicorn is a dependency; `python manage.py collectstatic --noinput` at build when the settings set `STATIC_ROOT`; no migrations at start | | FastAPI | the `fastapi` dependency and a module-level ` = FastAPI(...)` (tests and virtualenvs not searched; with several, the conventional `main.py`, `app.py`, `app/main.py` first) | `uvicorn : --host 0.0.0.0 --port $PORT` (`--app-dir src` for a src layout); uvicorn added when neither it nor `fastapi[standard]` is a dependency | | Flask | the `flask` dependency and a module-level ` = Flask(...)`, else ` = create_app()`, else `def create_app` | `gunicorn : --bind 0.0.0.0:$PORT` (`':create_app()'` for a factory); gunicorn added when it is not a dependency | | Streamlit | the `streamlit` dependency and the one top-level script importing it (`streamlit_app.py` among several) | `streamlit run