Skip to main content
Discord has no API that searches on behalf of a member, so mob maintains its own index of the messages the installed bot can see (api/src/mob/adapters/discord/events.py). Slack has such an API, so mob stores no Slack content at all; the Slack section covers that path.

What enters the index

The bot’s channel permissions set the boundary. Set up your Discord app covers how a private channel enters that coverage, and Authorization covers the second check at query time, where mob intersects the bot’s channels with the ones the requesting member can see. Inside those channels mob indexes:
  • message text, plus one line for each attachment filename, embed title and description, sticker name, and poll question
  • public threads and forum posts, stored under their parent channel so they inherit its visibility
  • messages from every author, apps and bots included
Private threads are never indexed. Their rows would sit under the parent channel id and reach members who cannot open the thread.

When messages become searchable

A live message is indexed as it arrives. History arrives behind it: each channel keeps a cursor recording the newest and oldest message stored, and a background task walks older pages one at a time, rotating across channels so recent coverage lands everywhere before deep history lands anywhere (api/src/mob/adapters/discord/backfill.py). A server that was just installed answers searches over its recent messages first, and older history keeps filling in for as long as the walk takes. The walk paces itself against Discord’s rate limits and resumes from the stored cursors after a restart. When the gateway reconnects, each channel re-indexes a short overlap behind its stored frontier, which absorbs edits made while the connection was away and removes messages deleted inside that window.

Attachment contents

Every attachment is searchable by filename from the moment its message is indexed. A second pass then downloads the file and adds its contents (api/src/mob/mirror/enrich.py):
  • text files, including source code and configuration formats, are decoded
  • CSV and TSV files contribute their header, a sample of rows, and a row count
  • PDFs contribute their text layer, so a scanned PDF carrying no text layer stays findable by filename
  • PNG, JPEG, WebP, and GIF images go through one vision completion on MOB_SMALL_MODEL, which transcribes legible text and describes what the image shows
Images need a configured model endpoint and spend a per-installation daily budget. The other formats need no key. Archives, video, audio, and office documents are never downloaded. Download size, extracted characters, PDF pages, and sampled rows all carry caps, and a file past a cap keeps its filename in the index (api/src/mob/mirror/extract.py).

How results are ranked

Every query runs Postgres full-text search over the indexed chunks. When the deployer configured a model endpoint, mob also embeds the query, runs a vector search, and fuses the two rankings (api/src/mob/mirror/search.py). Self-hosting covers the settings. Both legs degrade instead of failing. A query whose embedding call fails returns full-text results. Chunks whose embedding call failed during ingest are stored without a vector and stay findable through full-text search until a later edit or catch-up re-indexes them. A long message is split into chunks that are ranked individually, and each result carries the message it came from with a permalink.

How messages leave the index

  • An edit replaces the message’s chunks, and an edit that shortens a message drops the chunks past its new end.
  • A delete removes them.
  • A member running /mob delete inside the server removes every message that member authored from that server’s index.
  • Losing bot access to a channel, or deleting the channel or thread, purges its rows.
  • Disabling the installation from the console, removing the bot from the server, and uninstalling the app each drop the whole mirror for that installation.
A database loss is recovered by running the walk again rather than by restoring rows. Backups covers what that costs.

Slack

mob stores no Slack messages. Each query calls assistant.search.context with the requesting member’s own Slack token, Slack applies its own visibility rules, and the response stays in request memory until the MCP response completes. Slack indexes file contents itself, which is what search:read.files adds to the same call.