Skip to content

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.

  1. Install dependencies (corepack enable first, so the pinned pnpm version is used):

    Terminal window
    git clone https://github.com/colibri-social/colibri.social.git
    cd colibri.social
    pnpm install
  2. Create a .env from .env.example. Nothing in it is required for client work; PUBLIC_VAPID_KEY only matters if you’re testing Web Push, and must match the AppView’s VAPID_PUBLIC_KEY.

  3. Start the client dev server:

    Terminal window
    pnpm dev:client

    Then open http://127.0.0.1:4321. Vite binds 0.0.0.0:4321, but the OAuth redirect URI is registered against 127.0.0.1, so localhost will 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.

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.ts is 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’s did: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:

Terminal window
pnpm lint
pnpm test
pnpm --filter @colibri-social/client typecheck

Tests 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.

The website hosts the client, the marketing pages, and this documentation:

Terminal window
pnpm build
pnpm dev:web

The 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.

The shell renders the client’s built output, so build the client first:

Terminal window
pnpm --filter @colibri-social/client build
pnpm dev:tauri # desktop
pnpm dev:tauri:android # Android, needs the Android SDK/NDK
pnpm dev:tauri:ios # iOS, needs Xcode

Useful 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.

The workspace uses Biome for both:

Terminal window
pnpm lint # check
pnpm lint:fix # check and fix
pnpm format # format only

Run these before opening a PR. CI checks the same thing.

Changes to anything under apps/ or packages/ need a changeset, which drives versioning and the changelog:

Terminal window
pnpm exec changeset

Changes limited to the website’s content, the docs, or repository configuration don’t need one. More detail on the contributing page.

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:

Terminal window
pnpm changeset:feature

It 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:

Terminal window
pnpm whats-new:preview # writes a throwaway 0.0.0-preview release
pnpm dev:client
pnpm whats-new:generate # removes it again

The 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
<!-- whatsnew
title: Voice channels
icon: microphone-fill
body: 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.

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:

Terminal window
pnpm add https://pkg.pr.new/@colibri-social/client@156

The 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.