Meili CDC Docs

Team and security

Accounts with three roles, an owner who cannot be locked out, server-side sessions with CSRF protection, brute-force limits, an audit log, and credentials encrypted at rest.

Roles

RoleCan
viewerRead everything except users, invitations, SMTP and Telegram settings, MCP settings and tokens, the task export and the audit log. Search indexes and preview settings.
moderatorViewer, plus operate existing tasks: start, stop, pause, resume, restart, resync, reset checkpoint (one or in batch), run diffs and repairs, replay and discard dead letters, test task definitions, push to webhook tasks, cancel Meilisearch tasks, run destination health checks.
adminEverything: create, edit, delete, import and export tasks; connectors and destinations; index changes (create, delete, settings, primary key, swap, compact); dumps, snapshots and experimental features; users and invitations; SMTP and Telegram; alert rules; MCP; the audit log.
Settings Users tab listing members with their role, the primary administrator badge, last sign-in, and pending invitations
Members, roles and pending invitations.

Permission matrix

The server enforces a role per API route. Routes not listed below need viewer for GET and admin for everything else, so new routes are locked down by default. The dashboard hides or disables what your role cannot do, and the server answers 403 {"error":"requires role moderator"} if you try anyway.

RouteViewerModeratorAdmin
Any other GET (tasks, logs, metrics, schema check, diff report, DLQ, indexes, settings, documents, connectors, destinations, alert rules and history, overview, /metrics, events)yesyesyes
POST /indexes/{uid}/search, POST /indexes/{uid}/settings/previewyesyesyes
GET/PATCH /auth/me, POST /auth/me/passwordyesyesyes
POST /tasks/{id}/{action}, POST /tasks/batch (except delete)noyesyes
POST /tasks/test (test a task definition)noyesyes
POST /tasks/{id}/diff, POST /tasks/{id}/diff/repairnoyesyes
POST /tasks/{id}/dlq/replay, DELETE /tasks/{id}/dlqnoyesyes
POST /ingest/{id} (tasks without a signing secret)noyesyes
POST /meili/tasks/cancel, POST /destinations/{id}/health/checknoyesyes
GET /tasks/export, GET /users, GET /invitations, GET /settings/smtp, GET /settings/telegram, GET /settings/mcp, GET /mcp/tokens, GET /auditnonoyes
Every other POST, PUT, PATCH, DELETE (task create, update, delete, import; batch delete; connectors and destinations, including their test endpoints; indexes; dumps; snapshots; experimental features; users; invitations; settings; alert rules; MCP)nonoyes

Connector and destination tests need admin

docs/api.md and the dashboard list testing connectors and destinations as a moderator action, but the server's route table does not include POST /connectors/test, /connectors/{id}/test, /destinations/test or /destinations/{id}/test, so the default rule applies and they require admin; a moderator gets 403. Moderators can still test a task's connection with Test connection in the task wizard.

All routes are under /api/v1. Public routes (no authentication): GET /health, GET /auth/config, POST /auth/setup, /auth/login, /auth/logout, /auth/forgot, /auth/reset, GET /auth/invite/{token}, POST /auth/invite/accept, plus /healthz outside the API.

Adding people

  • Invite (Settings → Users, admin): enter an email and a role. The invitation link is valid for 7 days and works once. With SMTP configured it is emailed; otherwise (or as well) the link is shown once so you can send it yourself. The invitee chooses a name and password and is signed in.
  • Create directly: an admin can create an account with a password (API POST /api/v1/users), or on the server with meilisearch-cdc admin users create.
  • Passwords have 8 to 128 characters and are hashed with argon2id.

Password reset

  • Forgot password appears on the sign-in page only when SMTP is configured. It always answers the same way, whether or not the email exists, and emails a link valid for one hour. Using it sets the new password and signs the user out of other sessions.
  • An admin can set another user's password (that user's sessions are revoked), except the primary administrator's.
  • Each user can change their own password from the user menu (other sessions are revoked).
  • Without email, and for the primary administrator, use the CLI: meilisearch-cdc admin users reset-password --email … --generate. See recovery recipes.

The primary administrator

The first admin (created by the setup page or CDC_ADMIN_EMAIL) is the primary administrator, or owner. On data from an older version, the oldest admin becomes the owner at startup. In the dashboard and the API, for every caller, the admin token included:

  • nobody can change their own role;
  • nobody can change the owner's role or delete the owner (403);
  • only the owner can set the owner's password;
  • the last admin cannot be demoted or deleted (409), and you cannot delete yourself.

Only the server CLI transfers ownership (admin users set-owner, to another admin) or resets the owner's password. To demote or delete the owner, transfer ownership first.

Sessions and CSRF

  • Signing in creates a server-side session and sets the cookie cdc_session (HttpOnly, SameSite=Lax, Path=/). It is Secure on HTTPS, behind a proxy that sends X-Forwarded-Proto: https with CDC_TRUST_PROXY, or always with CDC_COOKIE_SECURE.
  • Sessions last 7 days and slide: activity extends them. Signing out revokes the session.
  • Cookie-authenticated writes (POST, PUT, PATCH, DELETE) must come from the same origin: the Origin header (or Referer) must match the request's host, otherwise 403 cross-origin request refused. Browsers send it automatically; bearer-token requests are not subject to it.
  • Admins can sign everyone out with meilisearch-cdc admin sessions revoke.

Rate limiting

WhatLimit
Failed sign-ins per client IP30 per 15 minutes
Failed sign-ins per account (email)10 per 15 minutes; a successful sign-in resets it
Forgot password requests per IPcount against the same IP limit, success or not
Wrong current password when changing one's own password10 per 15 minutes per user

A blocked request gets 429 with a Retry-After header and {"error":"too many attempts, try again later","retryAfter":n}. Behind a proxy, enable CDC_TRUST_PROXY so the limit applies to the real client IP.

Audit log

Settings → Audit log (admin) lists, newest first, who did what, when and from which IP:

  • sign-ins (auth.login), failed sign-ins (auth.login_failed), setup, invitation acceptance, reset requests and password resets;
  • every successful write through the API or the dashboard, recorded with the route as its action (for example POST /api/v1/tasks/{id}/{action}) and the path as its target;
  • every mutating MCP tool call (mcp.<tool>, with its arguments);
  • every CLI command, refused ones included (cli.<command>, user cli). Passwords are never recorded.

The most recent 20,000 entries are kept. API: GET /api/v1/audit?limit=200.

The static admin token

CDC_ADMIN_TOKEN (or CDC_ADMIN_TOKEN_FILE) defines one bearer token with the admin role, for scripts, CI, Prometheus and webhook pushers. It acts as a user named API token; the dashboard's sign-in page also offers Use an API token instead. The service keeps only its hash in memory. Rotate it by changing the variable and restarting. Generate a long random value:

openssl rand -hex 32
curl -H "Authorization: Bearer $CDC_ADMIN_TOKEN" https://cdc.example.com/api/v1/tasks

AI agents get their own revocable, role-scoped tokens instead; see MCP.

Secrets encryption

  • Connector passwords (in connection strings), DynamoDB secret keys, Meilisearch API keys, the SMTP password, the Telegram bot token and webhook signing secrets are encrypted with AES-256-GCM before they are stored.
  • Each ciphertext is bound to its record and field (associated data such as task:<id>:dsn or destination:<id>:apiKey), so a value copied into another record cannot be decrypted there.
  • Secrets stay encrypted in memory and are decrypted only when a connection is opened. The API never returns them: they come back as ••••••, and sending the mask back keeps the stored value.
  • The key is CDC_SECRET_KEY (a base64 string of exactly 32 bytes is used directly, anything else is a passphrase stretched with argon2id), or, when it is unset, a random key generated once in <data-dir>/secret.key (mode 0600). Setting CDC_SECRET_KEY later makes the service ignore secret.key, so rotate as below rather than just switching.
  • Passwords of user accounts are not encrypted but hashed (argon2id); MCP tokens and the admin token are stored or held as hashes.

Rotating the key

The service holds a key ring: the current key encrypts, and CDC_SECRET_KEY_PREVIOUS can still decrypt. At startup every secret still encrypted with the previous key (or stored in an older format, or in plain text) is re-encrypted with the current one.

  1. Generate a new key: openssl rand -base64 32.
  2. Set CDC_SECRET_KEY to the new key and CDC_SECRET_KEY_PREVIOUS to the old one. If the old key was the generated file, use its content: CDC_SECRET_KEY_PREVIOUS="$(cat /var/lib/meilisearch-cdc/secret.key)".
  3. Restart. The log shows re-encrypted stored secrets with the current key for tasks and connectors; destinations and notification settings are re-encrypted as they load.
  4. Check that tasks run and connectors test fine, take a backup, then remove CDC_SECRET_KEY_PREVIOUS and restart again.

Only one previous key is supported at a time. A value encrypted with a key that is not configured fails with value was encrypted with a key that is not configured (set CDC_SECRET_KEY_PREVIOUS). Give the admin CLI the same key variables as the service.

Meili CDC user guide · last updated 2026-10-26 · Edit this page on GitHub

meilisearch-cdc is open source. Meilisearch is a trademark of Meili SAS; this project is not affiliated with it.