Command line and keys
Bring your project's definition to disk, run the app on your own machine, and publish it back without leaving the terminal.
The Fabapp command line is the same project you build in Studio, seen from somewhere else. It is not a separate
product or a different app: pull brings the definition files into your folder, push sends back what you edited,
and dev runs the app locally on the same template the platform publishes with.
It ships with the MCP server, which is how Claude Code, Cursor and other assistants read and write in your project.
What you need
- Node 20 or newer. Check with
node --version. - The Builder plan or above. The command line is unlocked from Builder on. If your plan does not include it yet, the Keys and CLI screen shows what changes and takes you straight to the subscription.
Nothing else. The tool has not a single dependency: it holds a credential and talks to your account, and every package in the graph could reach both.
Authorising this machine
You never create a key by hand. The login command creates it for you:
npx @fabappai/cli login
It opens your browser on a Fabapp page showing a short code. You check that the code on screen is the one printed
in your terminal, authorise, and the machine is connected. On a server or in CI, with no browser, use --no-open
and open the address yourself.
This is the device flow, and it exists for a very concrete reason: a key you copy and paste travels through places keys should not travel, such as shell history, a chat channel, or a config file that ends up in a repository. Here the key is born inside your machine and is never displayed.
Where the credential lives
In the operating system's keychain. When the machine has no keychain, it goes into a 0600 file, and the command
says so. A tool that quietly picks the worse place teaches you it is always safe.
Read or write permission
By default, login asks for read and write. For a machine that only needs to look, ask for less:
npx @fabappai/cli login --scopes "read"
A read-only credential does not even see the write commands, in the terminal or in the MCP server. Advertising an operation that will answer 403 is worse than not advertising it: the assistant tries, fails, and tries again with different arguments, because the refusal looks like a problem with the request rather than with the permission.
Your first project on disk
If the project does not exist yet, start from the terminal too:
npx @fabappai/cli create "My app"
It creates the project and its first surface, and links the current folder to it. That was the last step you could only take in the browser. If the project already exists, link it by id:
npx @fabappai/cli link <project-id>
npx @fabappai/cli pull
link ties the current folder to a project, checking your access before writing anything. pull brings down
the definition files: fab.schema.json (the entities and their fields), fab.automations.json,
fab.settings.json, fab.connectors.json, and each screen's fab.config.json.
Edit, and send it back:
npx @fabappai/cli push
Running it on your machine, and publishing
npx @fabappai/cli dev
On the first run, dev downloads the app and assembles a workspace in .fabapp/workspace on the same template the
platform publishes with, installs the dependencies and starts Vite. On later runs it reuses what is already there.
Use --app <surface> when the project has more than one surface, and --port to change the port.
Edit what is under src/, watch it in the browser, and publish:
npx @fabappai/cli deploy
deploy does two things, in this order: it uploads what you changed and only then publishes. The order matters.
Publishing before uploading would publish what is on the server, the command would look like it worked, and your
work would be left behind. To upload without publishing, so you can review in Studio first, use --no-publish.
Libraries and plugins
Installing a library works the way you expect: npm install <package> inside .fabapp/workspace, and deploy
takes along the packages you added since the workspace was assembled. Any package that runs in a browser will do.
Three things stay behind, and deploy tells you before uploading:
- The version of the platform's packages. React, Vite, Tailwind and the Radix components always come at the platform's version, because the SDK and the ready-made components are built on it. Changing the version here changes nothing in the published app.
- Build configuration.
vite.config.ts,index.htmlandtsconfigare the platform's and go back to the original on every publish. Apostcss.config.js, atailwind.config.jsor an@pluginline in a CSS file does not run in the build. - Files that are not text. The app's code is UTF-8 text, on the server and in the build. An image, a font or an
audio file in
public/stays home anddeploynames it; upload it in Studio (Brand, or the app's storage) and reference it by URL. A vector logo can stay in the code as an.svg. - Server-only packages, such as database drivers or libraries that need a secret key. The app runs in the visitor's browser; that code belongs in a backend function.
Build plugins come from a list. A plugin runs code inside the build, so the platform only switches on the ones it
has reviewed: for Tailwind, @tailwindcss/typography (long-form text), tailwind-scrollbar, tailwindcss-safe-area
(the phone's notch) and tailwindcss-motion (animations); for Vite, vite-plugin-svgr (SVG as a component),
vite-plugin-node-polyfills, @mdx-js/rollup (pages in MDX), vite-plugin-wasm and vite-plugin-glsl. To switch
one on, just install the package. The platform registers the plugin with its own settings, and dev does the same
on your machine, so what you see locally is what goes live.
What it will not do, on purpose
It will not delete. Neither in pull nor in deploy. A file that disappeared from your folder is
reported, never removed from the app. Your copy may be stale relative to the server (the AI may have edited
the app in Studio meanwhile), and deleting there because of an absence here destroys work nobody asked us to
manage.
It will not write over what you edited. dev assembles the workspace once; on later runs it compares and
warns you when the template drifted, instead of overwriting. Rebuilding is your call (--reset), and it tells you
that this deletes your edits.
It will not push what is not yours. deploy sends only what you changed. The platform's components, the SDK
and the app's shell stay where they are.
It will not hold an integration credential. fab.connectors.json and fab.integrations.json are read-only:
they say what the project has connected, so an assistant knows it exists and which operations to call, and never
the credential. Connecting and rotating happens on the platform.
Using it with Claude Code and other assistants
The MCP server uses the credential login already stored. It never asks for a secret and never shows one.
{ "mcpServers": { "fabapp": { "command": "npx", "args": ["-y", "@fabappai/cli", "mcp"] } } }
That block works for most clients. What changes is where it lives:
| Client | Where it goes |
|---|---|
| Claude Code | claude mcp add fabapp -- npx -y @fabappai/cli mcp, or the mcpServers block in ~/.claude.json |
| Claude desktop app | the mcpServers block in claude_desktop_config.json |
| Cursor | .cursor/mcp.json in the project, or ~/.cursor/mcp.json for all of them |
| VS Code (Copilot) | .vscode/mcp.json |
| Windsurf | its MCP configuration file, in the Codeium folder |
| Cline / Roo | the file the extension's own MCP panel opens |
| Zed | the context servers block of settings.json |
| Codex CLI | its config.toml, which declares the same command in TOML |
| opencode | the mcp block of opencode.json, as a local server running that command |
| Antigravity | its MCP configuration, from the editor's own MCP panel |
The command is our part and does not change. Where it goes is each client's decision and moves between versions: when this table disagrees with your client's documentation, the documentation is right.
The tool that matters most is fabapp_read_definition: without the schema at hand, a wrong model id answers 404
and an invented field answers 422. It is the first thing an agent should call.
What the assistant sees depends on the permission you granted: a read-only credential does not receive the write
tools. And fabapp_write_definition refuses to run before fabapp_docs has been called in the same conversation,
because a schema written without the contract invents field types and access rules, and both fail silently.
Seeing and revoking connected machines
In the panel, open Profile and go to Keys and CLI. The screen lists every authorised machine with the name it reported, the permission it was granted, and when it was last used.
Revoking is immediate and applies server-wide: the revoked machine stops working on its next call, even with the
credential still on its disk. The other direction is not true: running logout on a machine forgets the
credential there, and it stays valid on the server until it is revoked here. When a laptop goes missing, this
screen is what settles it, not logout.
When something does not work
"Your plan does not include the command line." Login only issues a credential from Builder on. The Keys and CLI screen names the plan you need.
The browser code does not match the terminal one. Do not authorise. Close the page and run login again: both
codes come from the same request and only differ when you are looking at an authorisation that is not yours.
push complains about a file the app cannot overwrite. It says which one. That is better than saving with a
200 and changing nothing.
Another API. FABAPP_API_URL changes the base address, which defaults to https://api.fabapp.ai.