File size: 17,901 Bytes
b17baae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# Self-Hosted Stack

MicProtocol ships a complete self-hostable integration fleet in `docker-compose.yml`. Every service replaces a paid SaaS equivalent, giving you maximum data ownership, no per-seat licensing, and zero vendor lock-in.

This doc is the unified operations reference. For per-integration API behavior and webhook side effects, see [Third-Party & Self-Hosted Integrations](./third-party-integrations.md) and [Webhooks](./webhooks.md).

## Architecture

```mermaid
graph TD
    API[MicProtocol NestJS API :4000] --> POSTGRES[(PostgreSQL pgvector :5432)]
    API --> REDIS[(Redis :6379)]
    API --> MEILI[(Meilisearch :7700)]
    API --> MINIO[(MinIO :9001)]
    API --> MICRT[mic-realtime :4001]

    API -. webhook .-> CALCOM[Cal.com :3002]
    API -. webhook .-> FORMBRICKS[Formbricks stack :3001]
    API -. webhook .-> CHATWOOT[Chatwoot :3005]
    API -. webhook .-> DOCUMENSO[Documenso :3004]
    API -. webhook .-> LISTMONK[Listmonk :9000]
    API -. webhook .-> LIVEKIT[LiveKit :7880]

    CALCOM --> POSTGRES
    FORMBRICKS --> POSTGRES
    CHATWOOT --> POSTGRES
    DOCUMENSO --> POSTGRES
    LISTMONK --> POSTGRES
    UMAMI --> POSTGRES

    CALCOM --> REDIS
    CHATWOOT --> REDIS
    FORMBRICKS --> REDIS

    CALCOM --> MINIO
    DOCUMENSO --> MINIO
    FORMBRICKS --> MINIO

    MAILHOG[Mailhog :8025] -. SMTP .-> ALL[All services]

    style API fill:#e1f5e1
    style MICRT fill:#fff3e0
```

## Service Catalog

### Core Infrastructure

| Service | Container | Image | Port(s) | Database |
|---|---|---|---|---|
| PostgreSQL | `micprotocol-postgres` | `pgvector/pgvector:pg16` | 5432 | All databases (via `init-databases.sql`) |
| Redis | `micprotocol-redis` | `redis:7-alpine` | 6379 | n/a (in-memory) |
| Meilisearch | `micprotocol-meilisearch` | `getmeili/meilisearch:v1.6` | 7700 | n/a (own volume) |
| MinIO (S3 storage) | `micprotocol-minio` | `minio/minio:latest` | 9001 (API), 9090 (Console) | n/a (own volume) |
| MinIO bucket creator | `micprotocol-minio-mc` | `minio/mc:latest` | - (one-shot) | n/a |
| Mailhog (dev SMTP) | `micprotocol-mailhog` | `mailhog/mailhog:latest` | 1025 (SMTP), 8025 (UI) | n/a |

### MicProtocol Apps

| Service | Container | Port | Notes |
|---|---|---|---|
| API | `micprotocol-api` | 4000 | tRPC + REST + Swagger at `/api/docs` |
| Background worker | `micprotocol-worker` | - | Same app image as the API (no published ports); runs BullMQ WorkerHosts incl. the Python clip-scoring subprocess |
| mic-realtime (Erlang/OTP) | `micprotocol-mic-realtime` | 4001 | Chat fanout + presence |
| Web | `micprotocol-web` | 3000 | Next.js 14 |
| Admin | `micprotocol-admin` | 3006 | Next.js platform admin |

### Self-Hosted Integrations

| Service | Container(s) | Port | Replacement | DB schema |
|---|---|---|---|---|
| Cal.com | `micprotocol-calcom` | 3002 | Calendly | `calcom` |
| Formbricks v4 | `micprotocol-formbricks` + `-hub` + `-cube` + 2 migrate jobs | 3001 (web), 8080 (hub), 4000 (cube) | Typeform / SurveyMonkey | `formbricks` (pgvector) |
| Documenso | `micprotocol-documenso` | 3004 | DocuSign / PandaDoc | `documenso` |
| Chatwoot | `micprotocol-chatwoot-web` + `-worker` | 3005 | Intercom / Zendesk | `chatwoot` (pgvector) |
| Listmonk | `micprotocol-listmonk` | 9000 | Mailchimp / ConvertKit | `listmonk` |
| Umami | `micprotocol-umami` | 3003 | Google Analytics / Plausible cloud | `umami` |
| LiveKit | `micprotocol-livekit` | 7880 (HTTP), 7881 (TCP), 7882 (UDP) | Twilio Video / Zoom SDK | n/a |

### Formbricks v4 substack

```mermaid
graph LR
    MIGRATE1[migrate job] --> DB[(Postgres formbricks DB)]
    MIGRATE2[hub migrate job] --> DB
    HUB[formbricks-hub :8080] --> DB
    HUB --> CUBE[cube.js :4000]
    CUBE --> DB
    WEB[formbricks :3001] --> HUB
    WEB --> CUBE
    WEB --> DB
    WEB --> REDIS[(Redis)]
```

Containers in the Formbricks substack:

1. `micprotocol-formbricks-migrate` (one-shot, runs Prisma migrations)
2. `micprotocol-formbricks` (web app, `:3001`)
3. `micprotocol-formbricks-hub-migrate` (one-shot, runs goose + river migrations)
4. `micprotocol-formbricks-hub` (Hub API, `:8080`)
5. `micprotocol-formbricks-cube` (Cube.js analytics, `:4000`)

Formbricks-specific runbook: [`formbricks-runbook.md`](./formbricks-runbook.md).

## Port Map

Fixed ports in `docker-compose.yml` — change in `docker-compose.yml` and matching env vars together:

| Port | Service |
|---|---|
| 3000 | Web (also guest portal) |
| 3001 | Formbricks |
| 3002 | Cal.com |
| 3003 | Umami |
| 3004 | Documenso |
| 3005 | Chatwoot |
| 3006 | Admin |
| 4000 | API |
| 4001 | mic-realtime |
| 5432 | Postgres |
| 6379 | Redis |
| 7700 | Meilisearch |
| 7880 | LiveKit HTTP |
| 7881 | LiveKit TCP |
| 7882 | LiveKit UDP |
| 8025 | Mailhog UI |
| 9000 | Listmonk |
| 9001 | MinIO API |
| 9090 | MinIO Console |
| 1025 | Mailhog SMTP |

## Database Layout

`init-databases.sql` runs on first Postgres boot and creates one database per integration:

| Database | Owner | pgvector | pgcrypto |
|---|---|---|---|
| `micprotocol` (default) | MicProtocol API | - | yes |
| `chatwoot` | Chatwoot | yes | yes |
| `formbricks` | Formbricks | yes | yes |
| `calcom` | Cal.com | - | yes |
| `documenso` | Documenso | - | yes |
| `listmonk` | Listmonk | - | yes |
| `umami` | Umami | - | yes |

If a volume already exists, `init-databases.sql` will not run. To bootstrap missing databases on an existing volume:

```bash
docker exec -i micprotocol-postgres psql -U micprotocol -d micprotocol < scripts/init-databases.sql
```

## Shared Object Storage (MinIO)

MinIO is the default storage backend for every self-hosted integration that uploads files. `minio-createbuckets` is a one-shot service that creates:

| Bucket | Owner |
|---|---|
| `micprotocol-storage` | MicProtocol API (media, headshots, clips) |
| `calcom-uploads` | Cal.com |
| `documenso-uploads` | Documenso |
| `formbricks-uploads` | Formbricks |

`micprotocol-storage` is set to public-read (used for guest-facing media). The others are private.

In production, point MicProtocol at Cloudflare R2 instead of MinIO by changing `R2_ENDPOINT`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, and `STORAGE_PUBLIC_URL` — no code changes required, both speak the S3 API.

## Shared SMTP (Mailhog in dev)

Every self-hosted integration sends outbound mail through the same SMTP settings:

```
SMTP_HOST=mailhog          # in compose network
SMTP_PORT=1025
SMTP_USER=                 # empty for Mailhog
SMTP_PASSWORD=             # empty for Mailhog
EMAIL_FROM=noreply@<yourdomain>
```

In production, swap Mailhog for your transactional relay (Resend, SES, Postmark, or self-hosted Postal/Haraka) by setting `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, and `EMAIL_FROM` to the provider's values. The integrations read these env vars directly — no code changes.

## Shared Redis (namespaced)

Chatwoot, Formbricks, and Cal.com all share the `micprotocol-redis` instance. Chatwoot and Cal.com namespace keys internally; Formbricks uses Redis for rate-limiting and Cube.js queue. There is currently no isolation beyond provider namespacing — for high-throughput production, split into one Redis per integration.

## Co-located Secrets

Every self-hosted service reads its secrets from environment variables that the API also needs. The complete list lives in [`.env.example`](../.env.example). Critical guidance:

| Category | Examples | Rotation policy |
|---|---|---|
| Postgres / Redis passwords | `POSTGRES_PASSWORD`, `REDIS_PASSWORD` | Rotatable; requires service restart |
| Encryption keys (immutable) | `FORMBRICKS_ENCRYPTION_KEY`, `DOCUMENSO_ENCRYPTION_KEY`, `CALCOM_ENCRYPTION_KEY`, `NEXTAUTH_SECRET` variants | Never rotate after first start; old encrypted data becomes unrecoverable |
| Webhook signatures | `*_WEBHOOK_SECRET` | Rotatable; update on both sides simultaneously |
| Admin / API tokens | `CALCOM_API_KEY`, `LISTMONK_API_TOKEN`, `CHATWOOT_API_KEY` | Rotatable via the admin UI of each integration |
| App secrets | `UMAMI_APP_SECRET`, `FORMBRICKS_CRON_SECRET` | Rotatable |

Use `bash scripts/generate-secrets.sh` to produce strong random values for every variable in one shot. See [Getting Started → Environment Configuration](./getting-started.md#3-environment-configuration) for details.

## Service-Specific Configuration Notes

### Cal.com

- Uses its own `calcom` database (Prisma migrations need a clean schema).
- `NEXT_PUBLIC_SINGLE_ORG_SLUG=micprotocol` and `ORGANIZATIONS_ENABLED=0` disable paid multi-org features for self-hosted single-tenant use.
- Generate an API key in Cal.com Settings → API Keys; set `CALCOM_API_KEY` in `.env`.
- Encrypts DB columns with `CALCOM_ENCRYPTION_KEY` (24-char hex, **never rotate** after init).

### Formbricks v4

- Five containers, must start in order: `formbricks-migrate` → `formbricks-hub-migrate` → `formbricks-cube` → `formbricks-hub` → `formbricks`. Compose `depends_on` chains handle this automatically.
- `SKIP_STARTUP_MIGRATION=true` prevents the web container from re-migrating (the dedicated migrate job already did it).
- `SKIP_STARTUP_MIGRATION` must remain `true`; running migrations inside the web container will race with the migrate job.
- Analytics require Cube.js (`CUBEJS_API_SECRET` must match between web and cube containers).
- Sign-up is disabled without an invite (`EMAIL_VERIFICATION_DISABLED=1`).

### Documenso

- Requires a signing certificate. Generate with the commands in [`scripts/documenso-cert/README.md`](../scripts/documenso-cert/README.md).
- Mount the cert directory read-only at `/opt/documenso/cert`; the app looks for `cert.p12` inside.
- `DOCUMENSO_ENCRYPTION_KEY` and `DOCUMENSO_ENCRYPTION_SECONDARY_KEY` are both required and **never rotated**.

### Chatwoot

- Rails + Sidekiq worker pair (`chatwoot-web` + `chatwoot-worker`).
- Uses the `chatwoot` database with `pgvector` (required by Chatwoot itself).
- Storage is local (`RAILS_STORAGE_LOCATION=/app/storage`, mounted as `chatwoot_storage` volume). For larger deployments, switch `ACTIVE_STORAGE_SERVICE` to S3/MinIO.
- `FRONTEND_URL` must match the public URL or the live-chat widget will fail to load.
- Generate `SECRET_KEY_BASE` with `openssl rand -hex 64`.

### Listmonk

- Container runs `./listmonk --install --idempotent --yes && ./listmonk --upgrade --yes && ./listmonk --config ''` on first boot — idempotent, safe on restarts.
- `LISTMONK_db__database=listmonk` (must match the name in `init-databases.sql`).
- API token is generated in Listmonk Admin → Settings → API; set `LISTMONK_API_TOKEN` in `.env`.
- `LISTMONK_ADMIN_USER`/`LISTMONK_ADMIN_PASSWORD` bootstrap the super-admin on first run.

### Umami

- Single container, Postgres-backed.
- `UMAMI_APP_SECRET` is required; generate with `openssl rand -base64 32`.
- Add a website in the Umami UI and copy the website ID to `NEXT_PUBLIC_UMAMI_WEBSITE_ID`.

### LiveKit

- SFU for the in-app recording studio. MicProtocol owns the browser client (`apps/web`); LiveKit owns the WebRTC media routing.
- Dev defaults (`devkey` / `devsecret`) work in Compose but **must be changed for production**.
- Webhooks from LiveKit back to the API call `POST /integrations/webhooks/livekit` and trigger `RecordingSession` state transitions.
- For internet-facing deployments, expose UDP `7882` and configure `rtc.use_external_ip: true` (or run behind a TURN server).

### mic-realtime

Erlang/OTP service that owns:

- Chat fanout (per-workspace conversation messages)
- Presence (who is online in a workspace)
- Live domain events (published by the NestJS API via `/internal/dispatch`)

Key env vars:

| Var | Default | Notes |
|---|---|---|
| `MIC_REALTIME_PORT` | 4001 | HTTP listener |
| `MIC_REALTIME_HOST` | 0.0.0.0 | Bind address |
| `MICPROTOCOL_API_URL` | http://api:4000 | Used by mic-realtime to call back into the API |
| `MIC_REALTIME_INTERNAL_TOKEN` | (change me) | Bearer used by API to publish events |
| `MIC_REALTIME_SHARED_SECRET` | (change me) | JWT signing secret for WS auth tokens |
| `MIC_REALTIME_PUBLIC_URL` | ws://localhost:4001 | Public WS URL handed to web clients |
| `MIC_REALTIME_MAX_MSG` | 65536 | Max message size in bytes |
| `MIC_REALTIME_RATE_LIMIT` | 120 | Messages per minute per connection |
| `MIC_REALTIME_PRESENCE_TTL` | 90 | Presence TTL (seconds) |

Both `MIC_REALTIME_INTERNAL_TOKEN` and `MIC_REALTIME_SHARED_SECRET` must be strong random strings in production.

#### Production deployment

`mic-realtime` ships in every deployment topology:

| Topology | Where | Public route |
|---|---|---|
| Local dev | `docker-compose.yml` (service `mic-realtime`) | `ws://localhost:4001` |
| Coolify | `docker-compose.coolify.yml` (service `mic-realtime`, internal `:4001`) | `wss://realtime.<root>` via Caddy |
| Render | `render.yaml` (`micprotocol-realtime`, `rootDir: services/mic-realtime`) | `wss://micprotocol-realtime.onrender.com` (or custom domain) |

The [Caddyfile](../Caddyfile) routes `realtime.<root>` to `mic-realtime:4001`
(the specific host block wins over the `*.` wildcard). Caddy handles the
WebSocket upgrade transparently on the same route, so one URL serves both the
`GET /healthz` probe and `GET /ws?token=...`.

Secrets are **fail-closed**: `RealtimeClient`
(`apps/api/src/chat/realtime.client.ts`) and `mic_config.erl` both refuse to
start in production when `MIC_REALTIME_INTERNAL_TOKEN` /
`MIC_REALTIME_SHARED_SECRET` are missing or equal to the known defaults.
Generate strong values (`openssl rand -hex 32`) and set
`MIC_REALTIME_PUBLIC_URL` to the public `wss://` URL; the web/admin frontends
receive it as `NEXT_PUBLIC_MIC_REALTIME_URL`. The Coolify stack defaults the
API→realtime base URL to the internal `http://mic-realtime:4001`
(`MIC_REALTIME_URL`), so API→realtime callbacks never leave the Docker
network.

> ⚠️ Render **free** instances sleep after ~15 min of inactivity and cap at
> 750 hours/month — a sleeping WebSocket engine drops every live chat/presence
> socket. Use `plan: starter` or higher for realtime in production.

## Start / Stop / Update

### Bring up the full stack

```bash
docker compose up -d
```

### Bring up only infra (for local app dev)

```bash
docker compose up -d postgres redis meilisearch minio minio-createbuckets
```

### Rolling update a single integration

```bash
docker compose pull formbricks
docker compose up -d --no-deps formbricks
```

Formbricks v4 migrations need the migrate jobs to re-run after a version bump:

```bash
docker compose up formbricks-migrate
docker compose up formbricks-hub-migrate
docker compose up -d --no-deps formbricks formbricks-hub formbricks-cube
```

### Tear down

```bash
docker compose down            # keep data
docker compose down -v         # wipe all volumes (destructive)
```

## Health Endpoints

| Service | Endpoint |
|---|---|
| API | `GET http://localhost:4000/health` |
| Cal.com | `GET http://localhost:3002/api/health` |
| Formbricks web | `GET http://localhost:3001/api/health` |
| Formbricks Hub | `GET http://localhost:8080/health` |
| Formbricks Cube | `GET http://localhost:4000/formbricks/readyz` (via cube container) |
| Chatwoot | `GET http://localhost:3005/health` |
| Listmonk | `GET http://localhost:9000/health` |
| Documenso | `GET http://localhost:3004/api/health` |
| Umami | `GET http://localhost:3003/api/health` |
| MinIO | `GET http://localhost:9001/minio/health/live` |
| Meilisearch | `GET http://localhost:7700/health` |
| mic-realtime | `GET http://localhost:4001/healthz` |
| LiveKit | `GET http://localhost:7880/health` (HTTP only; UDP port 7882 for WebRTC) |
| Mailhog | UI at `:8025` always returns 200 |

The API exposes a consolidated integration health check at:

```bash
curl -s http://localhost:4000/integrations/health | jq
```

This pings each self-hosted service's health endpoint in parallel and reports reachability.

### Integration Management Dashboard

The Admin panel (`apps/admin`) provides a full management interface for all self-hosted integrations:

| Feature | Description |
|---|---|
| **Health Monitoring** | Real-time health status, latency, and version for Chatwoot, Cal.com, Formbricks, Listmonk, Documenso, Umami, and LiveKit |
| **Webhook Secret Management** | View and rotate shared webhook secrets directly from the admin panel |
| **Admin Panel Links** | Direct "Open Admin" buttons that open each integration's native admin UI in a new tab |
| **Credential Management** | Connect, update, or disconnect API keys for self-hosted integrations |
| **Webhook Delivery Log** | Recent webhook deliveries with status, latency, and error details |
| **Provider Metrics** | Per-provider request counts, average latency, and error rates |

Access these features from the **Monitoring** and **Integrations** tabs in the admin navigation sidebar.

## Resource Limits

Default `deploy.resources.limits` in `docker-compose.yml`:

| Service | CPUs | Memory |
|---|---|---|
| API | 2 | 2G |
| Worker | 2 | 2G |
| mic-realtime | 1.5 | 1G |

Other services have no explicit limits — tune for your hardware. CLIP scoring (Python ML subprocess) runs inside the worker and is by far the most CPU- and memory-hungry: see [CLIP Scoring Engine](./clip-scoring.md) for scale guidance.

## Alternatives if You Skip Self-Hosting

If you don't want to run a particular self-hosted service, simply remove its block from `docker-compose.yml` and omit its env vars from `.env`. The MicProtocol API degrades gracefully — each integration's API client is called only when its corresponding URL env var is set. Missing integrations return `false` from the health check and the API logs a warning rather than 500-ing.

For the externally-hosted (non-self-hosted) integrations (Clerk, Resend, Cloudflare R2, NVIDIA NIM, DeepSeek, Moonshot, Stripe, Paystack, M-Pesa, Africa's Talking, Twilio, Google/Outlook calendars), see [Third-Party & Self-Hosted Integrations](./third-party-integrations.md).