Multi-tenant application that syncs project mailboxes (IMAP/SMTP) with issue trackers. Supported providers: GitLab (cloud + self-hosted) and GitHub (cloud + Enterprise Server).
- OIDC authentication (configurable IdP via env)
- GitHub and GitLab OAuth sign-in
- Teams, projects, member management
- Per-project IMAP polling + SMTP outbound
- Rules engine (match sender/subject/body → create issue with labels/assignee)
- Bidirectional sync: email → issue/comment, issue comment → email reply
- Loop prevention via bot-user filtering, sync markers, and note deduplication
- Webhook + polling fallback for outbound comments
- Docker Compose or Kubernetes (Helm) for self-hosting from this repo
| Layer | Technology |
|---|---|
| API | Bun + Hono |
| Worker | Bun + pg-boss |
| Frontend | Vite + React + Base UI + CSS Modules |
| Database | PostgreSQL + Drizzle ORM |
| Deploy | Docker Compose or Helm + Docker |
- Bun 1.2+
- Docker (for Postgres, GreenMail, Adminer, Roundcube)
cp .env.example .env
# Edit .env — at minimum DATABASE_URL, ENCRYPTION_KEY, SESSION_SECRET, and login providersbun installdocker compose up -dThis starts Postgres, GreenMail, Roundcube, and Adminer (local development only).
bun run db:generate # after schema changes only
bun run db:migratedb:migrate applies OSS migrations. When SB_EXTENSION_MANIFEST is set in .env, extension migrations run in the same command.
bun run devThis runs the API, worker, and web UI in parallel via Bun workspaces (bun run --filter './apps/*' dev).
Run a single app:
bun run dev:api
bun run dev:worker
bun run dev:web| Service | URL |
|---|---|
| Web UI | http://localhost:5173 |
| API | http://localhost:3000 |
| Dev mail UI (Roundcube) | http://localhost:8888 |
| GreenMail API / raw browser | http://localhost:8080 |
| Database UI (Adminer) | http://localhost:8081 |
Dev login (when LOCAL_LOGIN=true): dev@localhost / dev
To load a plugin extension (e.g. the private cloud edition), add to .env:
SB_EXTENSION_MANIFEST=../serviceboard-cloud/extension.config.tsThen install the extension repo and merge its env vars (see that repo's .env.example):
cd ../serviceboard-cloud && bun installRe-run bun run db:migrate after setting the manifest so extension tables are applied.
docker compose up -d also starts Postgres, GreenMail, Adminer, and Roundcube. These services are for local development only — they are not deployed to Kubernetes.
Typical daily flow:
docker compose up -d # if not already running
bun run db:migrate # after pulling new migrations
bun run dev # API + worker + web| Variable | Description |
|---|---|
DATABASE_URL |
Postgres connection string |
ENCRYPTION_KEY |
64-char hex key for AES-256-GCM secret encryption |
SESSION_SECRET |
Session signing secret |
WEBHOOK_BASE_URL |
Public URL for issue-tracker webhooks (GitLab/GitHub must reach this) |
TLS_CA_BUNDLE |
Optional path to a PEM CA bundle used for all provider API calls (merged with per-project CA) |
LOG_LEVEL |
Log verbosity (debug, info, warn, error; default: info) |
In development (NODE_ENV=development), the API and worker also append structured JSON logs to .logs/api.log and .logs/worker.log at the repo root. Use these when bun run dev truncates terminal output — e.g. tail -f .logs/worker.log for provider API errors including responseBody.
Servicebeard supports multiple login providers. Enable one or more by setting *_LOGIN=true and the required credentials (see .env.example). All OAuth/OIDC providers share one callback URL, defaulting to {WEB_URL}/api/auth/callback so the OAuth state cookie matches the browser origin (in local dev, register http://localhost:5173/api/auth/callback in your OAuth app). Override with OAUTH_REDIRECT_URI if needed.
Enabled with LOCAL_LOGIN=true. Supports credential sign-in and passkeys without an external IdP. In development, the API also seeds dev@localhost / dev on startup for quick testing.
| Variable | Description |
|---|---|
LOCAL_LOGIN |
true to enable, false or unset to disable |
LOCAL_LOGIN_SIGNUP |
Allow sign-up via local credentials (default: true when local login is enabled) |
Works with any OpenID Connect provider (Keycloak, Auth0, etc.).
| Variable | Description |
|---|---|
OIDC_LOGIN |
true to enable (requires OIDC_* config), false or unset to disable |
OIDC_SIGNUP |
Allow new users on first sign-in (default: true) |
OIDC_PROVIDER_NAME |
Display name on the login button (e.g. Keycloak → "Continue with Keycloak"; omit for "Continue with SSO") |
OIDC_ISSUER |
Issuer URL (e.g. https://auth.example.com/realms/myrealm) |
OIDC_CLIENT_ID |
Client ID |
OIDC_CLIENT_SECRET |
Client secret |
Register {WEB_URL}/api/auth/callback as the redirect URI in your IdP.
- Go to GitHub Developer settings → OAuth Apps → New OAuth App
- Set Authorization callback URL to
{WEB_URL}/api/auth/callback(local dev:http://localhost:5173/api/auth/callback) - Copy the Client ID and generate a Client Secret into
.env:
| Variable | Description |
|---|---|
GITHUB_LOGIN |
true to enable (requires GITHUB_* config), false or unset to disable |
GITHUB_SIGNUP |
Allow new users on first sign-in (default: true) |
GITHUB_CLIENT_ID |
OAuth App client ID |
GITHUB_CLIENT_SECRET |
OAuth App client secret |
Optional alternative to per-project personal access tokens. When configured, projects can authenticate with a GitHub App installation ID instead of a PAT. Issues and comments appear as your-app[bot].
- Create a GitHub App (see in-app docs under Provider setup → GitHub) with Issues, Webhooks, and Administration (read) permissions on repositories.
- Set the app Setup URL to
{API_URL}/api/github-app/setup(for local dev:http://localhost:3000/api/github-app/setup). - Generate a private key and note the App ID.
- Add server-wide credentials to
.envfor the API and worker:
| Variable | Description |
|---|---|
GITHUB_APP_ID |
Numeric App ID from the app settings page |
GITHUB_APP_PRIVATE_KEY |
PEM private key (use \n for line breaks in .env), or |
GITHUB_APP_PRIVATE_KEY_PATH |
Path to the .pem file (relative to repo root, or absolute) |
- In the project wizard, choose GitHub and click Install GitHub App — no manual installation ID is needed. When
GITHUB_APP_IDis not set, projects use a personal access token instead.
PAT alternative: create a dedicated machine/bot GitHub account, grant it access to the repository, and generate a fine-grained or classic PAT for that account — not your personal login — so created issues clearly come from ServiceBeard.
Works with GitLab.com or self-hosted GitLab.
- Create an application under User Settings → Applications (GitLab.com) or Admin → Applications (self-hosted)
- Set Redirect URI to
{WEB_URL}/api/auth/callback(local dev:http://localhost:5173/api/auth/callback) - Enable the
read_userscope - Copy Application ID and Secret into
.env:
| Variable | Description |
|---|---|
GITLAB_LOGIN |
true to enable (requires GITLAB_* config), false or unset to disable |
GITLAB_SIGNUP |
Allow new users on first sign-in (default: true) |
GITLAB_BASE_URL |
GitLab instance URL (default: https://gitlab.com) |
GITLAB_CLIENT_ID |
Application ID |
GITLAB_CLIENT_SECRET |
Application secret |
- Create an OAuth2 application at Linear → Settings → API → Applications
- Set Redirect URI to
{WEB_URL}/api/auth/callback(local dev:http://localhost:5173/api/auth/callback) - Enable the
readscope (included by default) - Copy Client ID and Client Secret into
.env:
| Variable | Description |
|---|---|
LINEAR_LOGIN |
true to enable (requires LINEAR_* config), false or unset to disable |
LINEAR_SIGNUP |
Allow new users on first sign-in (default: true) |
LINEAR_CLIENT_ID |
OAuth application client ID |
LINEAR_CLIENT_SECRET |
OAuth application client secret |
For production, register {WEB_URL}/api/auth/callback in each OAuth app (same host users sign in from).
Adminer is available at http://localhost:8081:
| Field | Value |
|---|---|
| System | PostgreSQL |
| Server | postgres (from Adminer container) or localhost (from host tools) |
| Username | servicebeard |
| Password | servicebeard |
| Database | servicebeard |
For a schema-aware view without an extra container, use Drizzle Studio:
bun run db:studioInbound (email → issue)
- Worker polls IMAP for unseen messages per active project
- Thread match via
Message-ID/References/ subject+sender - Existing thread → append as public comment on issue
- New thread → evaluate rules → create issue on the linked provider with metadata
- Optional acknowledgement email to the original sender (project setting)
Outbound (comment → email)
- Provider webhook (
note_eventson GitLab,issue_commenton GitHub) or polling fallback - Skip internal comments (
[internal]marker at the start or end, or GitLab internal notes), email-synced comments (sync marker), bot-authored notes, and already-processed notes - Send threaded reply email via project SMTP
New issues include a collapsible Support details footer with a link back to the ServiceBeard project and a reminder about the [internal] marker.
Run the unit test suite:
bun testGreenMail provides IMAP (3143) and SMTP (3025). Two mailboxes are preconfigured:
| Mailbox | IMAP/SMTP login | Password |
|---|---|---|
support@mail.test (project inbox) |
support |
support |
customer@mail.test (simulate a reporter) |
customer |
customer |
Webmail: Roundcube connects to GreenMail over IMAP. Standard folders (Sent, Drafts, Spam, Trash, Archive) are scaffolded at startup for both mailboxes. Log in with support / support or customer / customer (local-part username). When composing mail, use @mail.test addresses — Roundcube rejects single-label domains like @localhost. Log out and sign in as the other user to switch mailboxes.
GreenMail’s raw API/message browser is at http://localhost:8080.
Configure your project mail settings to use the support inbox for both IMAP and SMTP:
| Field | Value |
|---|---|
| IMAP host | localhost |
| IMAP port | 3143 |
| IMAP TLS | off |
| IMAP user | support |
| IMAP password | support |
| SMTP host | localhost |
| SMTP port | 3025 |
| SMTP TLS | off |
| SMTP user | support |
| SMTP password | support |
| From | support@mail.test (or Support <support@mail.test>) |
1. Inbound (email → issue) — send mail into the project inbox (swaks optional):
swaks --to support@mail.test \
--from customer@mail.test \
--server 127.0.0.1 --port 3025 \
--header "Subject: Help needed" \
--body "My app is broken"The worker polls IMAP and should create an issue on the linked provider (with a matching rule). View the message in Roundcube (support inbox).
If Send acknowledgement email for new issues is enabled on the project, the worker also sends a threaded reply to the inbound From address. The reply lands in the reporter mailbox — log into Roundcube as customer / customer and check the inbox. Use @mail.test addresses when composing mail; acknowledgements are sent to whatever address appears in From (misconfigured clients that send as @greenmail or @localhost will not deliver to the customer@mail.test inbox).
2. Outbound (comment → email) — add a public comment on the synced issue (GitLab issue or GitHub issue). The reply is sent via SMTP to customer@mail.test. Log into Roundcube as customer / customer to read it.
3. Reply (email → comment) — send a threaded reply back to the project inbox:
swaks --to support@mail.test \
--from customer@mail.test \
--server 127.0.0.1 --port 3025 \
--header "Subject: Re: Help needed" \
--header "In-Reply-To: <paste-message-id-from-greenmail>" \
--body "Thanks, here is more detail"The worker should append this as a comment on the existing issue instead of opening a new one.
Self-hosted / custom TLS: When creating a project, enable Skip TLS certificate verification for private CAs, or paste a PEM CA cert on the project. You can also set TLS_CA_BUNDLE=/path/to/ca-bundle.pem in .env to apply a CA bundle to all provider API calls.
GitHub projects: use repository slug owner/repo. When GITHUB_APP_ID is set on the server, use Install GitHub App in the project wizard (issues appear as your-app[bot]). Without it, provide a PAT from a dedicated bot account — classic repo scope or fine-grained token with Issues + Webhooks write. For GitHub Enterprise Server, set the instance URL and use the same owner/repo format.
GitLab projects: use numeric project ID or group/project path and a token with api scope.
Production stack: Postgres, API, worker, web (nginx), and Caddy (HTTPS) in deploy/compose/.
Prerequisites: a VPS with Docker Engine and Compose plugin, and a DNS A record pointing at the server.
cd deploy/compose
cp .env.example .env
# Edit .env — DOMAIN, ACME_EMAIL, POSTGRES_PASSWORD, ENCRYPTION_KEY, SESSION_SECRET, login providers
# Web image: embed VITE_* and CLOUD_PLAN_* vars from .env (see apps/web/vite.config.ts envPrefix)
bun run extract-vite-env.ts .env .env.vite
docker compose --env-file .env -f docker-compose.yml up -d --buildPoint your domain at the server before starting Caddy so Let's Encrypt can issue a certificate. The API container runs database migrations on startup.
With a plugin extension (e.g. the private cloud edition), set EXTENSION_DIR in .env to the extension checkout and add its compose overlay:
bun run extract-vite-env.ts .env .env.vite
docker compose --env-file .env \
-f docker-compose.yml \
-f /path/to/extension/deploy/compose.cloud.yml \
up -d --buildBuild images manually (OSS-only):
docker build --target api -t servicebeard-api .
docker build --target worker -t servicebeard-worker .
docker build --target web -t servicebeard-web .With an extension build context:
docker build --build-context extension=/path/to/extension -f Dockerfile --target api .The root docker-compose.yml is for local development only (Postgres, GreenMail, Adminer).
Container images and the Helm chart are published to GHCR. New packages default to private; a maintainer must set each package to Public once under Packages (Package settings → Change visibility).
From GHCR (recommended for self-hosting):
helm install servicebeard oci://ghcr.io/hongaar/servicebeard-helm \
--version 0.1.0 \
--set ingress.host=your-domain.com \
--set secrets.encryptionKey=<64-char-hex> \
--set secrets.oidcClientSecret=<secret> \
--set secrets.sessionSecret=<secret>The chart defaults to ghcr.io/hongaar/servicebeard-{api,worker,web}:latest. Pin a specific release with --set image.tag=<git-sha>.
From this repo (local chart):
helm dependency update deploy/helm
helm install servicebeard deploy/helm \
--set ingress.host=your-domain.com \
--set secrets.encryptionKey=<64-char-hex> \
--set secrets.oidcClientSecret=<secret> \
--set secrets.sessionSecret=<secret>In-app documentation lives under /docs in the web UI (mailbox setup, issue providers, GitHub/GitLab guides). Sync design and extension points are in ARCHITECTURE.md.
apps/
api/ Hono REST API + webhooks
worker/ pg-boss job processors
web/ React frontend
packages/
db/ Drizzle schema + crypto
providers/ Issue tracker adapters (GitLab, GitHub)
shared/ Zod schemas + types
deploy/
compose/ Production Docker Compose (API, worker, web, Postgres, Caddy)
helm/ Kubernetes Helm chart