Craig: a multi-track voice recorder for Discord
Craig is a multi-track voice recorder for Discord.
At a glance
- What is it?
- Craig is a Discord bot that records voice channels and splits the result into one audio file per participant. It is a TypeScript monorepo with a bot, a dashboard, a web app server, a task scheduler, a database layer and two audio components, deployed as a set of Docker Compose services with Postgres and Redis behind them.
- Who is it for?
- Adopt Craig if you run a Discord community that records voice channels and needs the result separated into per participant tracks, since that is the whole point of the project and the separation logic is what you would otherwise have to write. Do not adopt it if you want a lightweight script, because the architecture is a full service with a database, a scheduler and several deployable components.
- Can I use it commercially?
- Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the project actually is
Craig is a multi-track voice recorder for Discord, and the two adjectives are doing the work in that sentence.
Multi-track is the differentiator. A voice channel recording captured as a single mixed file is of limited use, because everyone is talking at once and the recording is a wall of sound. Splitting the same audio into one file per participant turns a recording into something you can actually use, listen to one person, skim a meeting, or publish a track of one speaker.
The mechanism is Discord voice itself. Discord's voice protocol carries per-user audio streams, and a bot that joins the channel can receive each participant's stream separately rather than the mixed result. Craig does that and writes the streams out as separate tracks. That is the whole product idea, and it is the kind of thing that a general screen recording tool cannot do because the separation happens at the protocol level rather than by un-mixing a finished recording.
The project is TypeScript, licensed ISC, with the homepage at craig.chat and the author recorded in the package manifest as Snazzah with a link to snazzah.com. The package is marked private, so the repository is not published as a library. The version field is 0.0.0, which is the conventional placeholder for a private workspace root rather than a released version.
The engine requirements are worth noticing for a bot: Node ^22.18.0 or >=24.11.0, and pnpm >=12.1.0. The package manager is pinned to [email protected], and there is a pnpm-lock.yaml and a pnpm-workspace.yaml at the root.
The project took part in Hacktoberfest, which the repository topics list, and it has a translation status badge pointing at a translation service, so the bot interface is localised rather than English only.
A monorepo with a specific set of components
The repository layout shows a service, not a script.
There are apps/ and packages/ directories, plus a patches/ directory, which is the pnpm convention for carrying modifications to dependencies. There is a locale directory, which holds the translation files the translation badge refers to. There is a scripts/ directory, an nginx.conf, a docker/ directory and a docker-compose.yml with a docker.env.example, so the whole thing is designed to be run as a set of containers. There is a Dockerfile, a .devcontainer directory for local development, and a tsdown.config.mjs, which is the bundler configuration.
The build tooling is Turborepo. The root package.json runs turbo for dev, lint, format and build, and the build script does a git submodule update before running the turbo build, which tells you that at least one part of the build comes from a submodule rather than from the workspace.
The package names give you the component list. The build-cook script targets kitchen and ennuizel-streamer, and those two names recur throughout the Docker configuration. Then there is bot, ferret, dashboard, webapp-server, tasks and db.
So the shape of the system is roughly: a bot that joins voice channels, two components that handle audio, kitchen and ennuizel-streamer, a dashboard, a web app server, a task scheduler called tasks, and a database package. The Dockerfile builds and deploys each of them individually with pnpm deploy, so they are separate deployable units rather than one bundled server.
The internal package naming explains the two audio components being separate. They are both involved in producing audio and they are separate projects, which suggests a division of labour between capturing and processing.
The root package.json also shows some of the operational tooling. There is a sync script and a sync:dev for the bot, a run-task script for the scheduler, and two administrative scripts that are worth calling out. entitlement runs scripts/manageEntitlements.mts, and redact-user runs scripts/redactUser.mts. A redaction script in a project that stores voice recordings is a meaningful feature to see, and an entitlements script suggests the bot has tiers or paid access.
The database, the scheduler and the configuration
The docker-compose.yml is the most informative file in the repository, because it shows what the running system needs.
The database is Postgres 17 on bookworm, with a named volume for its data and a healthcheck running pg_isready against the configured user and database every ten seconds with a five second timeout and five retries. The Redis service is Redis 7 on bookworm with a healthcheck that runs redis-cli ping. The bot service itself is built from ghcr.io/craigchat/craig with a tag that defaults to latest, and it mounts a shared storage directory containing three subdirectories, rec for recordings, downloads and output.
The three separate storage directories are a design decision rather than tidiness. Recordings, finished downloads and generated output are different things with different lifecycles, and splitting them means a cleanup job can expire recordings without touching the outputs that have already been produced. The .env.example confirms the arrangement with its own note that if the variables are empty they default to the project folder with a rec and an output folder, and that these folders must be created or the service may not work.
The database package is the db workspace, and there are four separate scripts for it. db:deploy runs the migrate deploy path through dotenv-cli, db:push and db:migrate:dev are the usual Prisma alternatives, and a prisma script exposes the Prisma CLI directly. So the database layer is Prisma on Postgres, with a checked-in migration history that has to be applied to the database before the bot runs.
The scheduler is the tasks package, and the .env.example shows what it runs. There is a Patreon refresh cron, a recording clean cron, and a timezone setting for the scheduler, with the cron expressions given as examples, one running hourly and the other every thirty minutes. There are also Patreon campaign, client and secret variables, a tier map, a credentials file, and a set of recording controls: a fallback expiration defaulting to 24h, a list of skip IDs, and a skip-all switch.
The recording fallback expiration is the one that matters operationally. A recording that is never completed, because the session ended badly, would otherwise sit on disk forever. An expiration gives it a deadline.
The bot also needs a Discord token and application id, which the .env.example does not include. That omission is a small sign that the secrets are documented elsewhere, in the documentation the README points to, rather than in the example file.
Building it, and the Codespaces path
The contributing section is short and points at two ways in.
The first is a Codespace. The readme says you can start a Codespace which installs all the necessary things, and the badge links to a quickstart template. For a project with this many services, a hosted environment that is already wired up is a much cheaper way to make a first contribution than assembling the toolchain locally.
The second is the local route, and it is a pnpm monorepo with a specific sequence:
git clone https://github.com/CraigChat/craig
cd craig
pnpm i
pnpm build
pnpm build-cook # builds the dependencies for kitchen
pnpm db:deploy # updates the databaseThe order of those five steps is the order that matters. The install comes first, then the build, then the build-cook step for the kitchen dependencies, and the database migration last.
The build-cook step is the least obvious of the four. Its comment says it builds the dependencies for kitchen, and the root package.json shows it as pnpm -F kitchen -F ennuizel-streamer run build-cook, so it is a shared script run against two specific packages. The Dockerfile does the same thing, running the build-cook script for @craig/kitchen and @craig/ennuizel-streamer as separate steps before the turbo build.
Whatever cook does, it produces something the main build depends on, which is why it is called out separately rather than folded into build. The Dockerfile then removes the intermediate artefacts afterwards, the dist folders, the build folders and the cook directories, since pnpm deploy needs a clean tree to package each service for production.
The db:deploy step at the end is the one that catches people out. A freshly cloned database has no schema, and the bot will not start against an empty Postgres until the migrations have been applied. dotenv-cli is in the dependency list precisely so that the scripts can load the environment file before running the underlying command, which means db:deploy works without you exporting the variables yourself.
Deployment shape and the packaging details
The Docker build is the clearest statement of how this thing is meant to run.
It starts from node:24-trixie-slim as the build stage, with corepack enabled for pnpm, and installs build-essential, ca-certificates, git, python3, unzip and wget. The presence of build-essential and python3 is not incidental, because two of the workspace packages have native build steps that need a compiler and a Python interpreter. The pnpm store is mounted as a cache so that the install step does not re-download the dependency tree on every build.
The build stage then runs the cook scripts for the two audio packages and a turbo build filtered to the seven services, and finally deploys each one into its own directory under /opt/craig with pnpm deploy using the legacy flag, which is the flag that makes pnpm produce a self-contained node_modules tree rather than symlinks into the workspace. After that it deletes the intermediate build outputs.
That last step matters for the production image, because a deploy with symlinks back into the workspace would break the moment the build stage was discarded. The clean-up is what makes the copied trees independent.
On the Compose side, the craig service publishes four ports, with host port variables for the dashboard, ferret, ennuizel and the web app. The web app is reached over a websocket, and the compose file points it at ws://127.0.0.1:9002/shard, so the web app is a websocket server that the browser connects to for live updates. That is the right shape for a dashboard that shows recordings appearing as they finish.
The nginx.conf at the repository root suggests there is a reverse proxy in front of the whole set, which is how a single hostname ends up serving the dashboard, the downloads and the websocket endpoint.
Finally, there is the version and release plumbing. The build takes a CRAIG_VERSION argument that defaults to unknown, and the compose file uses a CRAIG_IMAGE_TAG that defaults to latest, with the image published to the GitHub Container Registry as ghcr.io/craigchat/craig. The project is ISC licensed, and the last push was on 2026-10-22.
Editorial conclusion
Adopt Craig if you run a Discord community that records voice channels and needs the result separated into per participant tracks, since that is the whole point of the project and the separation logic is what you would otherwise have to write. Do not adopt it if you want a lightweight script, because the architecture is a full service with a database, a scheduler and several deployable components. Before you commit, read the licence and the recording consent implications in your community's rules, and set up the database migrations before the first run, since the bot expects a migrated schema.
Frequently asked questions
What is Craig?
It is a multi-track voice recorder for Discord. Rather than producing one mixed recording of a voice channel, it captures each participant's audio separately, so a recording can be listened to per speaker. The project is TypeScript, ISC licensed, with the homepage at craig.chat and the author recorded as Snazzah.
What are the components in the Craig monorepo?
The workspace contains a bot, kitchen and ennuizel-streamer for the audio side, a dashboard, a webapp-server, a task scheduler called tasks, and a db package. The build is driven by Turborepo, and the Dockerfile builds and deploys each service separately with pnpm deploy into its own directory under /opt/craig.
How do I set up Craig locally?
Clone the repository, then run pnpm i, pnpm build, pnpm build-cook for the kitchen dependencies and pnpm db:deploy to apply the database migrations. The database step is last on purpose, because the bot needs a migrated schema. A Codespace is offered as an alternative that installs everything for you.
What does Craig need to run?
Postgres 17 and Redis 7 alongside the bot, according to the docker-compose file, with shared storage directories for recordings, downloads and output. The .env.example notes that the recording and output folders must be created or the service may not work, and it configures a recording fallback expiration defaulting to 24 hours so incomplete recordings are cleaned up.
How are Craig's Docker images built and published?
The Dockerfile builds from node:24-trixie-slim, installs native build tools including build-essential and python3 for the packages that need them, runs the cook scripts for the audio packages, then deploys each service with pnpm deploy and removes the intermediate build outputs. The image is published as ghcr.io/craigchat/craig with a tag that defaults to latest.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/craigchat-craig)Was this useful?