BellD / DevelopersDocumentation · latest

Getting started

Follow J1: connect an existing repository, qualify a revision, deploy to staging, and open its Surface. You need Node 22+ to run the CLI, access to a BellD workspace, and an application that meets the hosted-app descriptor and evaluation requirements. For a Python service, also read Python services. Replace every example ID with the ID returned by your environment.

Install and sign in

Run the following setup block once, from the BellD repository checkout. Both pnpm install and pnpm --filter @belld/cli build belong in that checkout; running them in your application directory targets that application's dependencies and can produce installation errors unrelated to BellD. The shell helper lets belld work after you change directories. If the CLI bin is already installed, the helper is unnecessary.

Replace /absolute/path/to/BellD below with the absolute path to your BellD repository checkout before running the setup block. Keep that same absolute path if you define the helper again after changing directories. Do not replace it with $(pwd) in your application directory: doing so would silently overwrite BELLD_CLI with the wrong path, breaking the helper with MODULE_NOT_FOUND. Run the install and build commands only in the BellD checkout.

pnpm install
pnpm --filter @belld/cli build
export BELLD_CLI="/absolute/path/to/BellD/cli/dist/bin.js"
belld() { node "$BELLD_CLI" "$@"; }

Sign in and select your workspace using belld:

belld login --api-url https://api.staging.belld.ai --issuer https://auth.staging.belld.ai/realms/belld --client-id belld-cli
belld workspace list
belld workspace use ws_REPLACE

Use the staging API URL for subsequent commands (or pass --api-url each time). Before running init, change to the directory where you want the local .belld/project.json mapping written. The application source is selected by --repo and --ref, or by --archive; the current working directory selects the mapping location. When both --repo and --ref are supplied, this directory does not need to be a git checkout of that repository. Without --archive, any missing --repo or --ref falls back to the local git remote or branch, respectively.

export BELLD_API_URL=https://api.staging.belld.ai
belld init --repo https://github.com/OWNER/APP.git --ref main --json

For a private repository, add it to the BellD GitHub App first. Then run belld github connections to find the connection's credential ID, and pass it to belld init with --credential.

For an existing BellD service, use belld link prod_REPLACE instead of init. Both paths write the local project mapping. The mapping contains workspace and service IDs, never a credential. Keep the service ID returned by init or link. Service IDs start with prod_: the CLI flag is --product and the API paths use /products.

Inspect and confirm the revision

belld status --json
belld doctor
belld revision list --product prod_REPLACE --json
belld revision get rev_REPLACE --json
belld revision confirm rev_REPLACE --product prod_REPLACE --json
belld capability inspect rev_REPLACE --json
belld binding status --product prod_REPLACE --revision rev_REPLACE --json

The revision ID that belld init returns is the intake revision. Until you confirm it, belld revision get returns 404 for that ID. To review the detected descriptor before confirming, open the service in the Console or call GET /v1/workspaces/{ws}/revisions/{rev}/draft-descriptor with that ID. After belld revision confirm, use the confirmed revision ID it returns.

Select the revision created by this intake. Confirm only after reviewing its detected descriptor. Required workspace-owned capabilities need appropriate credentials and provider bindings. See the capability catalog and credential command. Supply secrets through stdin or --from-env NAME. Platform-owned capabilities follow their own catalog requirements.

Declare capabilities in package.json

An app that calls a BellD capability declares it in package.json under belld.capabilities. Each entry has id, required, actions and scope; provider is optional. For example:

{
  "belld": {
    "port": 3000,
    "health": { "path": "/healthz" },
    "capabilities": [
      {
        "id": "ai.generate",
        "required": true,
        "actions": ["generate"],
        "scope": { "models": ["text"], "max_tokens_per_call": 1000 },
        "provider": { "allow": ["openai_compatible"], "fallback": "none" }
      }
    ]
  }
}

belld.port is the port your server listens on (default 3000). belld.health.path is the path BellD checks to see that your app is up (default /). Call a capability only through the SDK, inside belld.withRequest(req, fn), with a literal call such as belld.ai.generate( or belld.data.kv.set( in a file that imports @belld/sdk. Qualification stops if your code calls a capability or an action that you did not declare; declaring one that your code never calls only produces a warning.

Static sites

If a Node project has no server entrypoint (no scripts.start, main, index.js or server.js), has a build script, and has index.html and a vite.config file in its root folder, BellD builds it and serves the dist/ folder with node /belld/static-server.mjs. Paths without a file extension fall back to index.html; a missing path whose last segment contains a dot, such as /u/john.doe, returns 404. If the build writes to another folder, edit the start command to node /belld/static-server.mjs followed by that folder. Each file can be at most 10 MiB, and the page at the health path (default /) at most 64 KiB. Build-time variables such as VITE_* cannot be set yet, and a Vite base other than / makes assets return 404. A project imported before static hosting was available needs a new import to be detected again. A preview of a service without a name can show a blank page until its subdomain is ready.

Port and health check

Your server must listen on the port in belld.port (default 3000). BellD reaches it from inside the container on 127.0.0.1, so listening on 127.0.0.1 or 0.0.0.0 both work. Your app counts as up when a GET request to the health path answers with a status from 200 to 399 within 30 seconds.

The scan does not treat a key as a secret when it is a JWT whose role is anon, public or publishable, such as a Supabase anon key, so you can put it in client code. A JWT with any other role, such as a Supabase service_role key, fails the scan with secret_in_source; store it as a secret variable instead.

Build-time downloads

During dependency installation, BellD can download only from the public npm registry (registry.npmjs.org). During the build command, it can also reach Google Fonts (fonts.googleapis.com and fonts.gstatic.com), so next/font/google works. Any other download fails, including install scripts that fetch binaries (such as Prisma engines), package files hosted outside npm, and build scripts that fetch from other hosts.

Monorepos

When you create a service from a monorepo, set its subdirectory: Subdirectory in the Console or --root in the CLI. BellD reads package.json, its scripts and the Node version from that subdirectory; for a Python service, it reads pyproject.toml and the dependency list from there. If the subdirectory has its own npm, pnpm or Yarn lockfile, dependencies are installed there. If it has none and the repository root has a pnpm-lock.yaml, yarn.lock, package-lock.json or npm-shrinkwrap.json, BellD installs dependencies at the repository root with that lockfile, then runs the build and start commands in the subdirectory. The subdirectory must be a workspace of the root project; otherwise the build fails because its dependencies are missing.

- Installing at the root installs the dependencies of every workspace in the repository, so builds take longer. The build time limit stays the same. - BellD removes every .npmrc file in the repository before installing, so settings such as node-linker or shamefully-hoist are not applied. A monorepo that needs them has to build without them. - BellD installs with pnpm 10.29.2 or Yarn 4.9.2 and ignores the packageManager field. A lockfile those versions cannot use unchanged fails the install. - If several services from the same repository have auto-deploy turned on, each push rebuilds all of them, even when it changes only one of their subdirectories. - Dependency vulnerabilities are checked across the whole repository, so findings from other projects in the monorepo also appear on this service. - Automatic fixes are not available for a service that installs dependencies at the repository root. - With Yarn, set nodeLinker: node-modules in .yarnrc.yml. With Plug'n'Play, a start command that runs node directly cannot load the dependencies. - For a service imported before root installs were available, only revisions created from a later commit install at the repository root.

Python services

BellD treats a folder as a Python service when it has a pyproject.toml or a requirements.txt and no package.json. It builds and runs Python services with Python 3.14 only; if pyproject.toml sets requires-python, it must allow 3.14.

Commit exactly one dependency list: uv.lock, poetry.lock (from Poetry 2), or a requirements.txt that pins every package with ==. A requirements.txt must also list the packages your packages depend on, because BellD installs only what is listed; the output of pip freeze has them all. Development dependencies are not installed.

BellD installs only prebuilt wheels, so every package needs a wheel for Linux x86_64 that works with Python 3.14; a package that publishes only source code cannot be installed. Packages come from PyPI, and during installation BellD can download only from PyPI. The one exception is a py3-none-any wheel committed in your repository, such as the Python SDK.

Put BellD settings in pyproject.toml under [tool.belld]. Written as JSON, [tool.belld] must fit in 4096 bytes.

KeyWhat it setsDefault
startThe command that starts your serverDetected, see below
portThe port your server listens on8000
health{ path = "/healthz" }: the path BellD checks to see that your app is up/
schedulesUp to 10 entries { path = "/tasks/daily", cron = "0 3 * * *" }: BellD calls the path on a 5-field UTC cron, at most every 5 minutesnone
egressHosts your server may connect to, for example ["api.openai.com", "db.example.com:5432"]; see Outbound connectionsnone
capabilitiesThe BellD capabilities your code calls; see the Python SDKnone

If you do not set start, BellD detects it when your dependencies include exactly one of FastAPI, Flask or Django:

FrameworkAlso needsStart command BellD uses
FastAPIuvicorn, and exactly one of main.py, app.py or app/main.py, with the application object named appuvicorn main:app --host 0.0.0.0 --port 8000
Flaskgunicorn, and exactly one of main.py, app.py or app/main.py, with the application object named appgunicorn main:app --bind 0.0.0.0:8000
Djangogunicorn, and exactly one folder with both wsgi.py and settings.pygunicorn mysite.wsgi --bind 0.0.0.0:8000

The module name follows the file or folder (for example app.main or mysite), and the port follows port. In any other case, set start yourself, and make your server listen on port. The health check works as for Node: your app is up when a GET request to the health path answers with a status from 200 to 399 within 30 seconds.

To call BellD capabilities from Python, use the Python SDK.

Python packages in your dependency list are checked for known vulnerabilities, as npm packages are. BellD cannot fix them automatically: update the package in your dependency list and create a new revision.

The CLI understands Python services too: belld verify reads [tool.belld] using Python 3.11 or later on your computer, belld doctor checks your dependency list and how it references the SDK, and belld hand-back gives FastAPI and Flask examples.

Frontend and backend in one app

When an app has exactly two services, one frontend and one backend, BellD adds a forwarding rule the first time the backend deploys successfully to staging or production: requests to the frontend's /api/ paths go to the backend, with the path unchanged. The frontend is a static site or a Next.js app with no API routes of its own; the backend can be Node or Python.

Because the path is not changed, the backend's routes must start with /api/, for example APIRouter(prefix="/api") in FastAPI or a Blueprint with url_prefix="/api" in Flask. You can change or delete the rule in the Console; BellD does not add a rule again after you delete it.

On the Free plan, at most two deployments in a workspace are awake at the same time, so a frontend and a backend that are both awake use both.

Evaluate qualification

belld evaluate rev_REPLACE --intent qualification --json
belld pipeline-run get pipe_REPLACE --json

Evaluation polls within its timeout. Continue only when the response has qualification.status equal to qualified. A successful HTTP response or exit 0 alone does not mean qualification passed. Inspect findings and use belld explain rev_REPLACE when it stops. Correct the application, create a new intake/revision, and evaluate again.

Read qualification.status from the belld evaluate response or belld pipeline-run get; these are the CLI commands that return it. The pipeline_runs in belld status --json are summaries: they contain neither qualification nor evidence, and their timestamps are unreliable. Do not use those summaries to determine the evaluation result. Evaluating an already evaluated revision is idempotent: it returns the existing run immediately without rerunning evaluation.

If the latest qualification run was blocked by a usage limit or stopped by a platform failure, run belld evaluate rev_REPLACE --intent qualification --rerun to start a new run. Each revision allows at most 3 qualification runs.

With local refresh credentials from belld login, polling in belld evaluate automatically renews the access token on HTTP 401 and retries the request once. External tokens supplied through BELLD_TOKEN or --token have no refresh token, so HTTP 401 still exits with code 3. Exit 3 does not mean evaluation failed: the pipeline run continues on the platform. Use belld pipeline-run get pipe_REPLACE --json to check the same run until it reaches a terminal state; replace an expired external token, or remove it and run belld login to use local refresh credentials. Each pipeline-run GET reads once, so repeat it as needed.

Release and deploy

belld release create --product prod_REPLACE --revision rev_REPLACE --json
belld deploy rel_REPLACE --environment staging --json
belld deployment get dep_REPLACE --json

Use the returned release and deployment IDs. Creating a release from a revision requires exactly one version; use --version instead of --revision if needed. Recheck deployment with a bounded deadline until it reaches its terminal state. Deployment GET reads once; it does not poll. If deployment fails, inspect its status/reason and belld explain dep_REPLACE before retrying.

Access the application

Take surface_id from the deployment result. If you did not save the deployment or Surface ID, belld status with --json lists the latest revision's deployments, including surface_id, so you can recover them. The CLI has no deployment list or surface list command.

belld status --json
belld surface get srf_REPLACE --json

Open the URL returned by the active Surface in a browser. Complete the access/sign-in flow required by the release audience; a published URL does not bypass access policy. Do not invent a hostname from resource IDs. This CLI has no access command.

See CLI behavior for polling and exit codes, Control API for resource responses, and J1 for the source coverage snapshot. The staging walkthrough is verified separately by the owner.

**Sign-in on your app's BellD address**

Requests to the address BellD gives your app pass the Authorization header through to your app. Cookies pass through only when their names start with __Host- (exact case); other cookies are not sent to your app, and your app cannot set them. This makes it harder for other apps to plant cookies on your address. If your app signs people in with other cookies, use a custom domain for now; we will update this page when all cookies pass through.

Protect your app against cross-site request forgery yourself. BellD passes the Origin, Sec-Fetch-Site, and Sec-Fetch-Mode headers through so you can check them.

Cookie names starting with __Host-belld_, in any letter case, belong to BellD. They are never sent to your app, on any address, and your app cannot set them.

Account recovery

If you cannot sign in to the BellD Console or the BellD Admin console, or with belld login, start from the BellD sign-in page: the page that opens when you sign in from the Console or the Admin console, or when you run belld login. Resetting a forgotten password or a lost authenticator app sends a link to the email address on your account.

Forgot your password: select Forgot Password? on the sign-in page and enter your username or email. Open the link in the email we send, then choose a new password. The link is valid for a limited time; if it has expired, request a new one.

Lost your authenticator app: use the same Forgot Password? link. After you open the email link, you are asked to set up a new authenticator app and then to choose a new password. Your previous authenticator app stops working.

Too many failed sign-ins: after 10 failed attempts in a row, sign-in is temporarily paused for up to 15 minutes. Wait, then try again.

Email not verified: you cannot sign in until the email address on your account is verified. Open the link in the latest verification email; the verification page also lets you send the email again.

No access to your email: BellD cannot yet recover an account without access to its email address.

Other sign-in problems: email info@bell-tech.com.

Support and abuse reports

Email info@bell-tech.com for help with BellD or to report an app that is being used for abuse. Support is by email only. We reply as soon as we can, but we do not promise a response time.

When you report abuse, include the app's address and what happened.