For developers

Take a piece of it.

Six of the pieces Gryt is made of are on npm. The design system, the tokens and the owl avatars are MIT, so you can drop them into something that has nothing to do with Gryt. The bot SDK, the voice engine and the APIs are here too.

An identity is a P-256 keypair that signs its own certificate. A server checks the signature instead of asking anyone, us included. There’s no bot account type, no bot token, and no way for a bot to skip any of it. What a bot can do is whatever an admin agreed to, checked the same way it’s checked for a person.

An owl from a name

Every avatar in Gryt is drawn from a nickname. The package that does it has no dependencies and never touches the network, so this page just runs it instead of showing you a picture. Type a name and both halves change.

The owl @gryt/owl draws for the name nora
nora
owl.ts
import { avatarSeed, owlAvatarDataUri } from "@gryt/owl";

const src = owlAvatarDataUri(avatarSeed("nora") ?? "gryt");

No network, no canvas, no dependencies. It’s a function from a string to SVG markup, and it runs the same in Node as it does here. avatarSeed lower-cases and trims, so someone who renames themselves and only changes the capitals keeps the same owl.

The packages

All six are used by the app you can download, and none of them needs a Gryt server to be useful. Four are MIT. The two AGPL ones only make sense pointed at Gryt anyway.

  • @gryt/uiMIT

    React components on Base UI, styled with Tailwind on the Gryt palette — what the desktop and web clients render.

  • @gryt/ui-nativeMIT

    The same design system through React Native: same tokens, same names, a different renderer.

  • @gryt/themeMIT

    The design tokens, colour scales and OKLCH maths, with no renderer and no DOM. The theme swatches on our front page come out of it.

  • @gryt/owlMIT

    The owl avatars — give it a name, get an SVG. No renderer, no DOM, no dependencies.

  • @gryt/voiceAGPL

    The voice engine on its own: signalling, ICE, tracks and audio, with web and React Native adapters.

  • @gryt/botAGPL

    Write a Gryt bot in TypeScript. It joins a server the way any other client does.

Bots

A bot joins a server the same way any other client does. A key it holds, a certificate it signed itself, and a challenge-response over P-256. From the server’s side it’s just another member.

  1. It starts up knowing only the address, says what it’s called and what it wants to be allowed to do, and gets turned away.
  2. It leaves a request behind. An admin opens Server settings → Bots and sees it.
  3. They untick anything they’d rather it didn’t have, and let it in.
  4. The approval reaches the bot without a restart. Leave it running.

What a bot asks for on its first run is the only list it ever gets. A later run asking for more gets the answer the first one got. That isn’t aimed at you. It’s aimed at the run that isn’t yours, after somebody takes over a published image. And if nobody is around for the first launch, like in a compose file or CI, an admin can decide it all up front and hand over a single-use token.

bot.ts
import { GrytBot } from "@gryt/bot";

const bot = new GrytBot({
  host: "chat.example.com",
  nickname: "Helper",
  wants: ["read_messages", "send_messages"],
});

bot.command("ping", async (ctx) => ctx.reply("pong"));

void bot.start();

bot.can() answers from what the server said, not from what you asked for. And it keeps up if an admin changes their mind while the bot is running.

A bot runs as a container. The example below builds on its own, since @gryt/bot comes off npm like any other dependency.

compose.yml
services:
  support-bot:
    build: .
    restart: unless-stopped
    environment:
      GRYT_HOST: chat.example.com
    volumes:
      - support-bot-identity:/data

volumes:
  support-bot-identity:

gryt-bot-identity.json is the bot. The id the server knows it by comes from the key inside it. Keep that file on a volume and the bot keeps its permissions across restarts and upgrades. Lose it and the server sees a stranger knocking, holding nothing.

Mounting the volume isn’t enough by itself. By default the bot writes that file next to the code, and only the bot can move it. identityPath is an option on GrytBot, and the SDK reads no environment variables of its own. The example passes process.env.GRYT_IDENTITY_PATH, and its Dockerfile sets that to /data/gryt-bot-identity.json. Miss it and the bot works, keeps its identity across restarts, and loses it the next time you rebuild the image.

Addons

An addon is a folder with an addon.json in it, loaded by the desktop app. A theme addon adds CSS. A plugin addon adds a module, and that module can talk to exactly one thing: an object on window.

pluginApi.ts
type ThemeInfo = { appearance: "light" | "dark"; accentColor: string };

interface GrytPluginAPI {
  version: string;
  theme: ThemeInfo;
  on(event: "themeChange", handler: (theme: ThemeInfo) => void): () => void;
}

declare global {
  interface Window {
    gryt?: GrytPluginAPI;
  }
}

And that’s all of it. No sandbox, no permission model, no registry, no docs page, and the plugin system is still down as planned on the roadmap. It’s enough to restyle the client or bolt something small onto it. It isn’t enough to build a product on.

What it should turn into hasn’t been decided yet. If you’ve tried to write one, an issue saying what you needed is more use than a feature request.

The APIs

Everything the apps use. The one call that needs nothing from you is /info, the join preview. It’s open on purpose, because a client has to be able to tell you whether you need an account before you try.

bash
curl -s https://chat.example.com/info
json
{
  "serverId": "...",
  "name": "Bird House",
  "members": "12",
  "lanOpen": false,
  "identityTiers": ["account"],
  "joinPolicy": "invite"
}

A server with discovery turned off answers 404 to anyone who isn’t already a member. The build number only comes back for members, because an open endpoint that names your exact version is a list of hosts for someone to scan.

The voice engine

@gryt/voice is the calling half of Gryt with nothing else attached — signalling, ICE, tracks and audio behind a set of React hooks. It talks to a Gryt SFU, and that is the only Gryt piece it needs.

JoinButton.tsx
import { SFUConnectionState, useSFU } from "@gryt/voice";

function JoinButton({ channelId }: { channelId: string }) {
  const { connect, disconnect, connectionState } = useSFU();

  if (connectionState === SFUConnectionState.CONNECTED) {
    return <button onClick={() => disconnect()}>Leave</button>;
  }
  return <button onClick={() => connect(channelId)}>Join</button>;
}

Two things have to be true above that, and both fail quietly. <VoiceSingletonHooks /> has to be mounted, or every singleton hook just hands back its starting value while the app builds and launches like normal. And Vite has to leave the package alone with optimizeDeps.exclude, or the RNNoise worker looks for itself somewhere it isn’t and you ship without noise suppression.

The design system

One set of tokens, two renderers, and a generator that turns a palette into a link. These three come straight out of the published package and are drawn here, picking up this page's colours as they go.

42 ms
app.tsx
import { Avatar, Button, Chip } from "@gryt/ui";

<Avatar seed="nora" alt="" size="small" />
<Chip label="42 ms" tone="success" />
<Button size="small">Send</Button>

The source

One superproject with the rest as submodules, each with its own CI and its own releases. The flag isn't optional. Without it you get thirteen empty folders.

bash
git clone --recurse-submodules https://github.com/Gryt-chat/gryt.git

If you’re here to run a server rather than build on one, the self-hosting page has the guides.