Client development
Everything a user sees lives in the colibri-social/colibri.social monorepo: the client library, the website that embeds it, the native shell, and these docs. It’s a pnpm workspace.
Directoryapps
- website Astro app: marketing site, docs, hosted web client
Directorypackages
- client the SolidJS UI
- wrapper the Tauri v2 desktop/mobile shell around the client
- lib shared helpers, XRPC types, events, facets, markdown
- assets fonts, sounds, logos, emoji, and their manifests
- standard-renderer renderer for standard.site documents
The client is packaged as a library because more than one shell consumes it. The website and the native app both mount the same App. See the client architecture page for the design behind that.
-
Install dependencies (
corepack enablefirst, so the pinned pnpm version is used):Terminal window git clone https://github.com/colibri-social/colibri.social.gitcd colibri.socialpnpm install -
Create a
.envfrom.env.example. Nothing in it is required for client work;PUBLIC_VAPID_KEYonly matters if you’re testing Web Push, and must match the AppView’sVAPID_PUBLIC_KEY. -
Start the client dev server:
Terminal window pnpm dev:clientThen open
http://127.0.0.1:4321. Vite binds0.0.0.0:4321, but the OAuth redirect URI is registered against127.0.0.1, solocalhostwill fail at login.
You also need the AppView running on 127.0.0.1:8000. In dev mode the client talks to it directly instead of proxying through your PDS, and mints its own service-auth tokens, so a local AppView isn’t optional, and the AppView switcher in settings is ignored.
Logging in locally
Section titled “Logging in locally”Login uses a real Atmosphere account through OAuth, against whatever PDS that account lives on. Two things trip people up:
- The early-access allowlist.
packages/client/src/atproto/allowlist.tsis a hardcoded set of DIDs, enforced in dev exactly as in production: a session for a DID that isn’t in it is cleared and you land on the waitlist screen. Add your own DID while developing. - Localhost OAuth clients. In dev the client id is the localhost development form,
http://localhost?scope=...&redirect_uri=http://127.0.0.1:<port>/app/login, instead of a hosted client metadata document. Scopes are built from the AppView’sdid:web, so they’re derived from what your local install points at.
CI runs Biome, the test suite, and a typecheck of the client, all of which you can reproduce locally:
pnpm lintpnpm testpnpm --filter @colibri-social/client typecheckTests are Vitest files sitting next to the code they
cover, configured by a small vitest.config.ts in each tested package.
Everything covered so far is plain TypeScript that runs in Node. The XRPC wrappers are the most valuable thing to
add to. Each one is a plain function of (fetch, ...args), so a mocked fetch
is the whole harness. See Testing for what belongs
where, the component sandbox, and how the AppView is kept in sync.
Running the website
Section titled “Running the website”The website hosts the client, the marketing pages, and this documentation:
pnpm buildpnpm dev:webThe website consumes the client’s built dist/, not its source, so build the workspace once before starting it (and rebuild the client after changing it, or run pnpm dev:client for UI work, which serves from source with HMR).
Its dev, build and preview scripts all run sync-assets first, which fetches shared assets out of @colibri-social/assets and copies them into place. TURSO_DATABASE_URL and TURSO_AUTH_TOKEN are declared as required server env vars, so the site won’t boot without them.
Documentation pages are Markdown/MDX under apps/website/src/content/docs/docs/, rendered by Starlight. The sidebar is assembled in apps/website/astro.config.ts: each group autogenerates from a directory, and page order inside a group comes from sidebar.order in the frontmatter.
Running the native app
Section titled “Running the native app”The shell renders the client’s built output, so build the client first:
pnpm --filter @colibri-social/client buildpnpm dev:tauri # desktoppnpm dev:tauri:android # Android, needs the Android SDK/NDKpnpm dev:tauri:ios # iOS, needs XcodeUseful flags, all documented in packages/wrapper/README.md: DISABLE_SENTRY strips Sentry from both the frontend bundle and the Rust binary (used for no-track distributions like F-Droid), and tauri build requires TAURI_SIGNING_PRIVATE_KEY because updater artifacts are enabled.
The native app can’t use a localhost OAuth client (the tauri://localhost origin isn’t publicly fetchable), so its client metadata is served from a fixed public origin. That means native login always goes through a hosted metadata document, even in development.
dev:tauri:android and dev:tauri:ios work for iterating on your own machine, but neither works on an Android emulator or an iOS Simulator. See Testing native builds for the build-and-install workflow those need instead.
Formatting and linting
Section titled “Formatting and linting”The workspace uses Biome for both:
pnpm lint # checkpnpm lint:fix # check and fixpnpm format # format onlyRun these before opening a PR. CI checks the same thing.
Changesets
Section titled “Changesets”Changes to anything under apps/ or packages/ need a changeset, which drives versioning and the changelog:
pnpm exec changesetChanges limited to the website’s content, the docs, or repository configuration don’t need one. More detail on the contributing page.
Announcing a change in-app
Section titled “Announcing a change in-app”The client shows a “What’s New” popup once per release, plus a settings page with the last five releases. Its content comes from the changesets themselves, so anything you want users to read about is written where you already write the changeset.
Use the wrapper instead of pnpm exec changeset, and it walks you through the entry after the usual prompts:
pnpm changeset:featureIt asks whether to announce the change (defaulting to yes for a minor or major bump and no for a patch), whether it is a feature or a fix, then a title, a body, and an icon. The icon step is a live search over Phosphor that narrows as you type, and it renders each match inline if your terminal supports the Kitty graphics protocol, as Ghostty, Kitty and WezTerm do. Set COLIBRI_NO_ICON_PREVIEW=1 to fall back to names only.
Only the -fill weight is offered, so the popup stays visually consistent. Search by the base name and the suffix is added for you: typing microphone finds microphone-fill.
To see your entry in the running app before it ships:
pnpm whats-new:preview # writes a throwaway 0.0.0-preview releasepnpm dev:clientpnpm whats-new:generate # removes it againThe popup opens once per unseen release, so if you have already dismissed it, clear lastSeenReleaseNote inside the colibri:user-preferences entry in local storage to bring it back. A browser profile with no stored preferences at all counts as a new install and is deliberately left alone.
The result is an HTML comment in the changeset body, which you can also write or edit by hand:
---"@colibri-social/client": minor---
Adds voice channels with screen sharing
<!-- whatsnewtitle: Voice channelsicon: microphone-fillbody: Hop into a voice channel and talk without leaving the app.kind: feature-->title, icon and body are required. kind is optional and defaults to fix for a patch bump and feature otherwise. A long body can wrap onto indented continuation lines. Colons inside values are fine, so no quoting is needed.
Two optional release-level keys, releaseTitle and heroImage, can be set by any one changeset in a release. Setting either from two changesets in the same release is an error.
CI fails a minor or major client changeset that has no block, so features never ship unannounced, and prints a ready-to-paste block in the failure. A patch without one only gets a hint, since most fixes are not worth a popup.
The generated files under packages/client/src/release-notes/ are written by pnpm ci:version during a release. If you edit them by hand, run pnpm whats-new:generate and commit the result, which CI also verifies.
Preview releases
Section titled “Preview releases”Every pull request gets an installable build of the client through pkg.pr.new without waiting for a release. A bot comment on the PR carries the install command:
pnpm add https://pkg.pr.new/@colibri-social/client@156The URL is keyed on the pull request number, so it keeps resolving to that PR’s latest build as you push to it. Commits on main publish too, which makes @colibri-social/client@main a way to track trunk between rc releases.
See also
Section titled “See also”- Client Architecture: how the client talks to an AppView.
- Development Overview: the dev-mode shortcuts in full.
- Facets: how rich text is encoded and rendered.
- Testing native builds: building and testing the native shell on each platform.