Micro Club Identity — the NestJS API that centralises Discord identity and access, and the Next.js admin panel that manages it. One pnpm + Turborepo monorepo.
apps/
api/ NestJS 11 API (Drizzle + Postgres + Redis) — deployed with Docker
web/ Next.js 16 admin panel — deployed on Vercel
cli/ mcid-cli, the MCID command-line client (MVP: Inbound Webhooks)
packages/
contracts/ Types and constants shared by api and web (compiled, CJS + .d.ts)
form-schema/ Inbound webhook FormSchema types, schema validator and condition evaluator, shared by api and cli
typescript-config/ Base / nestjs / nextjs / library tsconfig presets
eslint-config/ Shared ESLint flat-config base (typescript-eslint + prettier)
docs/ Historical design records (the current docs are in apps/web, see below)
Workspace packages are named @mcid/*: @mcid/api, @mcid/web, @mcid/contracts, @mcid/form-schema, @mcid/typescript-config, @mcid/eslint-config. The one exception is apps/cli, published to npm as mcid-cli.
The developer documentation is in the admin panel app, at /docs (http://localhost:3002/docs when you run it). It covers how to integrate a project with MCID, how to run and change MCID, the generated API reference, and the reasons behind past decisions. It is MDX in apps/web/src/content/docs, and tests keep it in step with the code. The files in docs/ are historical design records.
Requires Node.js >= 22 and pnpm 10 (corepack enable picks the pinned version from package.json).
pnpm install # once, at the repository root
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env
pnpm dev # api on :3000 and web on :3002 (needs Postgres + Redis, see below)The API needs Postgres and Redis. The easiest way is the dev compose stack, which also runs the API itself:
cd apps/api && docker compose up --build -d # see apps/api/README.mdRun from the repository root; Turborepo runs them per package in dependency order and caches the results.
| Command | What it does |
|---|---|
pnpm dev |
Watch-builds @mcid/contracts, runs the API and the web app |
pnpm build |
Builds everything (contracts → api, web) |
pnpm lint |
ESLint (note: the API's lint runs with --fix) |
pnpm typecheck |
tsc --noEmit in every package |
pnpm test |
Unit tests (Jest for the API, Vitest for the web and the CLI) |
pnpm test:e2e |
API end-to-end tests (needs Postgres, see .github/workflows/ci.yml) |
Target one package with a filter, e.g. pnpm turbo run test --filter=@mcid/web, or run a script inside it: pnpm --filter @mcid/api run db:migrate.
@mcid/contractsis a compiled internal package. The API is CommonJS and resolves modules the classic way, so a just-in-time TypeScript package would not load there. Every task that imports it depends on^buildinturbo.json, sopnpm build/test/typecheckbuild it first; inpnpm devit rebuilds on change. Keep it dependency-free and limited to things both sides really share (wire types, enum-like unions, theadmin_sessioncookie name, …).@mcid/form-schemais compiled the same way. It holds the FormSchema language of inbound webhooks: its types, the Layer 1 schema validator and the condition evaluator. The API validates schemas with it and the CLI pre-checks them with it, so the two can never disagree. Its only runtime dependency isvalidator.@mcid/typescript-configand@mcid/eslint-configare config-only packages. Apps extend them ("extends": "@mcid/typescript-config/nextjs.json",import base from '@mcid/eslint-config/base') and keep only their own overrides.- Dependency versions that must be pinned for security live in
overridesinpnpm-workspace.yaml. Some are scoped (jsdom>undici,msw>path-to-regexp) because a global pin broke the web test tooling — prefer scoped overrides.
Only the backend ships as a container. CI builds and pushes the image to GHCR on main:
docker build -f apps/api/Dockerfile . # run from the repository rootThe Dockerfile uses turbo prune @mcid/api --docker, so the image contains only the API and the workspace packages it depends on. Compose files live in apps/api/ (docker-compose.yml for dev, docker-compose.prod.yml for dokploy, docker-compose.fuzz.yml for fuzzing). See apps/api/DEPLOY.md.
The web app is not containerised. Import the repository in Vercel as a project with:
| Setting | Value |
|---|---|
| Root Directory | apps/web (keep Include source files outside of the Root Directory enabled) |
| Build Command | cd ../.. && pnpm turbo run build --filter=@mcid/web (builds @mcid/contracts first) |
| Ignored Build Step | npx turbo-ignore @mcid/web |
| Environment variables | NEXT_PUBLIC_API_URL (API URL including /api), NEXT_PUBLIC_APP_URL |
Add the Vercel domain to CORS_ORIGINS on the API.
.github/workflows/ci.yml runs lint + typecheck, unit tests with coverage, a full build and a check that the generated API docs are current for every push/PR to main and dev, API e2e tests against Postgres, and the API image build on main.
MIT, see LICENSE. Every workspace package is MIT too, including the ones bundled into the published mcid-cli.