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.
Set up
Section titled “Set up”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.
Get the code
Section titled “Get the code”git clone <repository-url> openbotcd openbotMake local settings
Section titled “Make local settings”devenv shell -- pnpm dev:setupdev: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.
Start the development server
Section titled “Start the development server”devenv shell -- pnpm devpnpm dev runs devenv up. It does these steps:
- It starts PostgreSQL 18 on
127.0.0.1, with the useropenbot_dev. It runs in Docker, from the same image as the production Compose file. Its data lives in the Docker volumeopenbot-dev-postgres. To reset the database, stoppnpm devand rundocker volume rm openbot-dev-postgres. - It starts RustFS, the object storage for files, on
127.0.0.1. It also runs in Docker. Its data lives in the Docker volumeopenbot-dev-rustfs. To delete the stored files, stoppnpm devand rundocker volume rm openbot-dev-rustfs. - It installs the packages with
pnpm install --frozen-lockfile. - It builds OpenBot and the packages it needs.
- It runs the database migrations.
- 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.
Checks
Section titled “Checks”| 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.
Repository layout
Section titled “Repository layout”| 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
Section titled “The docs site”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:
devenv shell -- pnpm --filter @openbot-build/docs devThen 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.
Project rules
Section titled “Project rules”Write for one reader
Section titled “Write for one reader”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.
Basic English
Section titled “Basic 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.
Headless
Section titled “Headless”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.
Changes to callers and data
Section titled “Changes to callers and data”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.
Effect data first
Section titled “Effect data first”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.
Type first
Section titled “Type first”Write the types before the code that carries out the work. A service contract is its set of inputs, outputs, errors, and needs.
- Define domain values and outside data with Schema. Use
typeof Name.Typeto share the type of a schema. Do not copy its fields into another type. - Give each
Context.Servicean explicit interface or object type. Define it beforemake. Do not useEffect.Success<typeof make>orReturnType<typeof make>to get the service type from its implementation. - State each method as
Effect.Effect<Value, Error, Needs>or a typed Stream. Use named errors. Keep generic errors asE, notanyorunknown. Keep needs at construction, except for context supplied by the caller. - 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.
Effect rules
Section titled “Effect rules”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,StringandRecordmodules of Effect in place of the native methods, imported asArr,StrandRec; ClockandDateTimefor time, theRandomandCryptoservices for random values and IDs, andConfigfor environment variables;HttpClientin place offetch, and theFileSystem,PathandChildProcessservices in place of the Node.js modules;- Effect Schema for JSON and for outside data, decoded through Effect and not
with the
*Syncfunctions; Schema.TaggedErrorfor errors, and a brand on each Schema class;Effect.genandEffect.fnin place ofasyncfunctions andnew Promise;it.effect,it.liveandit.layerfrom@effect/vitestin 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.
Small, real changes
Section titled “Small, real changes”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.
License
Section titled “License”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.
Package releases
Section titled “Package releases”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
mainpublishes0.0.0-commit-<full-hash>with the tagsha-<full-hash>. It does not changelatest. - A tag such as
v0.1.0publishes0.1.0with the taglatest. - 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.