Skip to content

Contributing

For developers. Start with the developer manual to choose API work, App publishing, or project changes.

This page is for people who change OpenBot itself. It shows how to set up your computer, run the server and the checks, and find your way in the code.


You need devenv. devenv gives you the exact tools for the project: Node.js, pnpm, PostgreSQL, and more. You also need Docker. The development database and the development object storage run in Docker, and you use Docker to build and check the images. devenv gives the Docker command line tools. Your computer only needs a running Docker service. See Deploy. Run every command through devenv shell -- <command>, so you always use these tools.

Terminal window
git clone <repository-url> openbot
cd openbot
Terminal window
devenv shell -- pnpm dev:setup

dev:setup makes .env.development with a new random secret key in OPENBOT_SECRET_KEYS. If your .env.development is from an older version, delete it and run dev:setup again. You can also reset the development database. See Configuration.

Terminal window
devenv shell -- pnpm dev

pnpm dev runs devenv up. It does these steps:

  1. It starts PostgreSQL 18 on 127.0.0.1, with the user openbot_dev. It runs in Docker, from the same image as the production Compose file. Its data lives in the Docker volume openbot-dev-postgres. To reset the database, stop pnpm dev and run docker volume rm openbot-dev-postgres.
  2. It starts RustFS, the object storage for files, on 127.0.0.1. It also runs in Docker. Its data lives in the Docker volume openbot-dev-rustfs. To delete the stored files, stop pnpm dev and run docker volume rm openbot-dev-rustfs.
  3. It installs the packages with pnpm install --frozen-lockfile.
  4. It builds OpenBot and the packages it needs.
  5. It runs the database migrations.
  6. It starts the development server at http://localhost. The server writes the platform settings for this origin itself, so there is no setup step.

devenv picks a free port for each service. It starts at a base port: 5433 for PostgreSQL, 9000 for RustFS, and 3000 for OpenBot. It shows the ports in its process list. The devenv proxy gives OpenBot the stable address http://localhost on port 80, whatever port OpenBot gets. On Linux, the proxy asks for your password with sudo the first time, so that it can listen on port 80.

When you change a file in packages/*/src, devenv starts OpenBot again.

The development server uses a local test account, test models, and a test App. No outside service is needed. To sign in and make a user token, see Quick start.


Command What it does
devenv shell -- pnpm install Installs all packages.
devenv shell -- pnpm build Builds every package with tsdown.
devenv shell -- pnpm typecheck Checks the types of every package and the Effect rules.
devenv shell -- pnpm rules Checks only the Effect rules of scripts/effect-rules.ts.
devenv shell -- pnpm -r test Runs the tests of every package.
devenv shell -- pnpm test Runs the tests of every package in one Vitest run, without build or type checks.
devenv shell -- pnpm --filter @openbot-build/core test Runs the tests of one package.
devenv shell -- treefmt Formats the code with Prettier and nixfmt.

Run pnpm build before pnpm typecheck. Packages import each other from their built files.

Tests use Vitest with @effect/vitest. A test that runs an Effect uses it.effect. It gives the test a test clock: time moves only when the test moves it. A test that needs real time, for example one with a real server, uses it.live. OpenBot tests start their own short-lived PostgreSQL server with the tools from devenv. They need no secrets and no outside services. GitHub Actions runs the same build, typecheck, and test commands. See Deploy.


Path What it holds
packages/ The workspace packages. See Packages.
packages/docs/ This docs site.
packages/template/ A starting point for a new package. It is not part of the workspace.
protocol-test/ The public test service for the App Protocol. See Protocol test.
deploy/ Files to deploy OpenBot. Each target has its own folder. See Deploy.
deploy/docker/ The Dockerfile, the Bake file, Compose files, the production environment template, and the image checks.
.env.development.example The template for .env.development.
patches/ Fixes for exact versions of outside packages.
references/ Source code of other open source projects, as Git submodules. Only for reading.
devenv.nix The development tools, settings, and processes.

The references/ folder is not part of the build. To read that code, fetch it with git submodule update --init.

To start a new package, copy packages/template/ to packages/<name>/. Then change its name, entry point, and dependencies.


The docs site is an Astro site built with Starlight. The build makes static files only. The logo in the header links to openbot.build. Start it with:

Terminal window
devenv shell -- pnpm --filter @openbot-build/docs dev

Then open http://localhost:4321. The English manual starts at /, Chinese at /zh-cn/, and German at /de/.

Path in packages/docs/ What it is
content/docs/*.mdx The pages. The file name is the URL: deploy.mdx is /deploy.
content/docs/<locale>/ The translations: zh-cn for Simplified Chinese and de for German.
astro.config.ts The site settings and the order of the pages in the sidebar.
src/components/ The components that pages can use without an import: Callout, Steps, and more.
src/i18n.ts The three languages of the docs.
src/pages/openapi.json.ts Serves openapi.json at /openapi.json, so people can download it.
scripts/api.ts Makes openapi.json from the API contract.

The API reference at /api is made from openapi.json by the starlight-openapi plugin. Do not write it by hand. The script runs before every dev and build. When you change the API contract in @openbot-build/api, the reference changes too. When you add a group to the contract, give it a title and a description in scripts/api.ts. The script stops with an error until you do. The build also makes /llms.txt and /llms-full.txt, which give the docs as plain text for AI tools. To check that every page builds, run devenv shell -- pnpm --filter @openbot-build/docs build.

Each push to main publishes the site to docs.openbot.build. See Publish the docs site.


Use the user manual for work through an existing client or Channel. Explain the task, result, and how to get help. Keep installation, API fields, and source code out of user pages.

Use the administrator manual for setup and service work. State whether a task needs organization rights, platform rights, or server access. Put client contracts, App publishing, and source changes in the developer manual. Link to another role when a task must pass to that role.

Keep code and field details in one place. Each translation must keep the same sections and link to its own language. The generated API reference stays in English.

Write all text in Basic English. This covers docs, code comments, error messages, logs, prompts, and test text. Use the 850 words of Basic English where you can, plus the technical words you need. Use short sentences. Explain each technical term the first time you use it.

English is the source for all text. The docs are also in Simplified Chinese and German. When you change an English page, change its translations in the same change. A translation keeps the same headings in the same order. Its links go to pages in its own language, such as /de/deploy. The API reference is only in English.

OpenBot has no user interface. Do not add pages, front ends, or client apps to the repository. The docs site is the only exception. Every new feature starts as an operation in the @openbot-build/api contract. Then the server handles it. Every operation must work with a user token and with the sign-in cookie. It must also say which action on which resource it needs, with a Required or GroupRequired annotation. The helpers own, organization, channel and channels in packages/api/src/Authorization.ts make the annotations. A requirement may only name a path parameter that its endpoint has. A test in packages/api checks this. See Permissions.

The authorization code comes from the ability library, the npm package @nmnmcc/ability. Version 3.1.0 is fixed in package.json. Policies use JSON text at HTTP and storage boundaries. This version makes the error classes public in its type declarations, so TypeScript can emit OpenBot declarations without a local patch.

Ask the user first when a change needs work for old callers, clients, or stored data. Do not choose between a breaking change and keeping old behavior on your own.

Never change the baseline migration. For each database schema change, add a new migration with devenv shell -- drizzle-kit generate --name <change>. Existing databases must be able to upgrade.

When you work around a bug in an outside package or tool, explain that bug in a comment next to the code.

A UUID is a 16-byte ID. Use UUID v7 when its time order helps stored records or indexes. Generate it with Crypto.randomUUIDv7() from Effect. Use the bytes format when the next step needs bytes. Keep UUID v4 for random secrets and for short-lived IDs that gain nothing from time order.

UUID v7 starts with the time in milliseconds. IDs made in the same millisecond have random order. Keep time fields and sequence numbers when exact order matters. See RFC 9562.

Store UUIDs as bytes where the data allows it. In PostgreSQL, use the native uuid type. It stores 16 bytes and accepts both v4 and v7. APIs can still use UUID text. Do not cast mixed IDs, outside IDs, or IDs with a prefix straight to uuid. Check their old values, links, and use in sealed data before a storage change. Add a new migration and test it with old v4 values.

Use the immutable data types of Effect, such as HashMap, Chunk, and Option. Use the mutable types of JavaScript only when you really need them.

Write the types before the code that carries out the work. A service contract is its set of inputs, outputs, errors, and needs.

  1. Define domain values and outside data with Schema. Use typeof Name.Type to share the type of a schema. Do not copy its fields into another type.
  2. Give each Context.Service an explicit interface or object type. Define it before make. Do not use Effect.Success<typeof make> or ReturnType<typeof make> to get the service type from its implementation.
  3. State each method as Effect.Effect<Value, Error, Needs> or a typed Stream. Use named errors. Keep generic errors as E, not any or unknown. Keep needs at construction, except for context supplied by the caller.
  4. Check the implementation with Service.of, then build its Layer. Local helper functions may use type inference. Shared types may use another declared contract, but must not depend on a constructor’s inferred result.

The service-contract check follows imported type aliases too. It stops a service whose type comes from make. The compiler also rejects any or unknown in error and context channels, unsafe Effect casts, missing service dependencies, and service provision outside the allowed boundaries. Tests may provide their own Layers.

The same check also tests module boundaries, including type imports. domain may use core, but not HTTP, SQL, or workflow adapters. core and protocol stay free of product packages. A feature in openbot must not import the app assembly folder. Packages use each other through public entries.

Business code and storage cannot import HTTP contracts. This includes PostgreSQL adapters, settings, models, accounts, roles, and SSO. These modules use domain values and errors. The API uses the same domain definitions and adds HTTP status codes. Only HTTP handlers, response mappers, and application assembly may import HTTP contracts. App adapters add protocol data at their boundary.

Business code cannot import HTTP response mappers. Shared error mapping uses domain errors. HTTP handlers add the response shape.

Server work uses the common execution engine. The checks reject local ChatRuntime use and Operations.make or Operations.layer in server code. Only the execution engine, resource transport, and application assembly may import Effect cluster or RPC modules. Domain code cannot use core execution or store adapters. These checks also follow import aliases and package subpaths.

OpenBot is written with Effect. Two checks keep the code in the Effect style. pnpm typecheck runs both, and GitHub Actions runs pnpm typecheck on every change.

The first check is the Effect language service. It is a plugin of the TypeScript compiler, set up in tsconfig.base.json. Its rules are errors, so tsc stops on them. Among other things, they ask for:

  • the Array, String and Record modules of Effect in place of the native methods, imported as Arr, Str and Rec;
  • Clock and DateTime for time, the Random and Crypto services for random values and IDs, and Config for environment variables;
  • HttpClient in place of fetch, and the FileSystem, Path and ChildProcess services in place of the Node.js modules;
  • Effect Schema for JSON and for outside data, decoded through Effect and not with the *Sync functions;
  • Schema.TaggedError for errors, and a brand on each Schema class;
  • Effect.gen and Effect.fn in place of async functions and new Promise;
  • it.effect, it.live and it.layer from @effect/vitest in tests.

Three rules are off. apiStabilityLeak and unstableApiUsage are off because the API contract is built on effect/http-api, which Effect marks as unstable. missingPipeableSignature is off because a pipeable form of each exported function helps a library, not an application.

The second check is scripts/effect-rules.ts. It reads every TypeScript file and checks the rules that the language service cannot see:

Rule What it stops
service-contract A service type taken from its constructor, including through imported aliases. Write its contract first.
blind-error-map Effect.mapError(() => ...), Effect.orElseFail(() => ...) and catch: () => ..., which drop the cause of a failure. A handler given by name counts too, when the same file declares it as a function with no parameter.
throw throw. Fail with a typed error, use Effect.die for a broken rule of the code, or assert.fail in a test.
run-outside-main Effect.runPromise and the other run functions outside a program entry point (a *Main.ts file). This includes runForkWith; an outside callback uses a scoped FiberSet runtime.
fork-daemon Effect.forkDetach. Fork a fiber in a scope, or keep it in a FiberSet or FiberMap.
promise-then Promise chains with .then. Turn the Promise into an Effect.
directive-reason A rule directive without a reason.

Some code must stay native, because an outside API asks for it or because Effect has no API for it. Examples are a Drizzle callback that must give back a Date, a Promise callback of an SDK, and HMAC signatures. Then turn the rule off for one line, and give the reason after --:

// @effect-diagnostics-next-line globalDate:off -- Drizzle calls this outside Effect and needs a Date.
// @effect-rules-next-line throw -- The SDK reads a thrown error as a failed call.

A directive is never a way to save work. Tests are program entry points, so strictEffectProvide is off in test/ folders.

Build what gives real value. Keep each change focused, and test the parts that can really break.

The full rules for contributors are in AGENTS.md at the root of the repository.


OpenBot uses the GNU Affero General Public License, version 3 only (AGPL-3.0-only). The full text is in the LICENSE file at the root of the repository. When you send a change, you agree that it uses the same license.

The client package uses MIT. This also covers the OpenBot code bundled with it, as set out in packages/client/LICENSE. Other packages keep AGPL. Client builds include the API contract at @openbot-build/client/Api and chat schemas at @openbot-build/client/Core. Apps do not need the other OpenBot packages.

CI builds, checks types, and tests before it publishes npm packages to GitHub Packages. All eight OpenBot release packages share one release version. The client keeps its MIT grant. The other packages use AGPL-3.0-only.

  • A push to main publishes 0.0.0-commit-<full-hash> with the tag sha-<full-hash>. It does not change latest.
  • A tag such as v0.1.0 publishes 0.1.0 with the tag latest.
  • Pull requests pack and check the release but do not publish it.

Use devenv shell -- pnpm packages:pack to make local archives in .release. Set RELEASE_REF=v0.1.0 to check a version release. Internal package dependencies use the same exact version. Packing restores the source manifests and does not change the workspace lockfile. Publication retries skip an identical upload; a version with other contents fails.

In GitHub Packages, give repositories that use OpenBot packages Actions read access to those packages. Their workflows use GITHUB_TOKEN with packages: read. If separate credentials are needed, set the PACKAGES_READ_TOKEN secret to a classic PAT with read:packages. Publishing uses each source repository’s GITHUB_TOKEN with packages: write. Local installs also need a token, even for public packages. See the client setup.