Configuration
There is no configuration UI — the tool is driven by a single JSON file in the working directory, discord-sync.json, plus a few command-line flags documented in Usage. One file holds three sections with three different owners:
settings— hand-edited tuning knobs (delays and Markdown frontmatter).guilds— what should be synced: your channel selections.state— what has been synced: the facts on disk. Auto-managed — never hand-edit it.
settings and guilds are yours to edit; state is rebuilt by update-state (which every sync runs), and it rewrites only the state section, preserving settings and guilds.
Every command that touches the file takes --config <FILE> (default discord-sync.json). The whole document is validated against a strict zod schema on load and before every write — guild and channel keys must be Discord snowflakes, directories must be non-empty — so a change that would corrupt it fails instead of being written. If the file does not exist, it is treated as an empty project.
{
"version": 1,
"settings": {
"assetDelayMs": 400,
"assetJitterMs": 400,
"exportDelaySeconds": 45,
"exportJitterSeconds": 30,
"frontmatter": { "explore": false }
},
"guilds": {
"686053708261228577": {
"name": "My Community",
"channels": {
"1262785282957119540": {
"name": "canvas-showcase",
"directory": "canvas-showcase"
}
}
}
},
"state": {
"channels": {
"1262785282957119540": {
"name": "canvas-showcase",
"guildId": "686053708261228577",
"guildName": "My Community",
"directory": "canvas-showcase",
"lastExportedAt": "2026-08-25T10:00:00Z",
"lastMessageId": "1408450000000000000",
"threadCount": 42,
"messageCount": 1234,
"threads": { "...": {} }
}
}
}
}
settings — tuning knobs (hand-edited)
settings is the one section you tune by hand. It has two kinds of knobs:
Pacing delays. assetDelayMs/assetJitterMs pace the asset downloader (a pause of assetDelayMs plus 0..assetJitterMs between downloads); exportDelaySeconds/exportJitterSeconds pace the export — the wait between chunked export windows and between sequential channel syncs.
Note: there are no per-message or per-thread delay knobs. DiscordChatExporter handles its own per-message rate limiting — it is always invoked with
--respect-rate-limits(honoring Discord’s advisory headers) and--parallel 1(a single request stream). The only pacing this tool adds is asset-download pacing and between-unit pacing (between chunks, between channels).
Markdown frontmatter. settings.frontmatter is a map of YAML keys/values merged into every generated Markdown file’s frontmatter. Values can be strings, numbers, booleans, or arrays of those. The explore flag is special: its precedence is the value already present in an existing Markdown file > the settings.frontmatter default > false. So curation you do in Obsidian survives regeneration, and the setting only seeds the default for freshly generated files.
guilds — selections (hand-edited)
guilds records what should be synced. It is written by the TUI (space toggles) and the select/deselect commands, read by sync-all and by the listings to show the ◇ marker. It maps each guild to the channels you chose, each with a display name and a target directory. Editing it by hand is fine, e.g. to change a channel’s target directory before its first sync. Deselecting the last channel of a guild removes the guild entry entirely.
state — what has been synced (auto-managed)
state.channels is rebuilt by update-state (which every sync runs) from the export files themselves; read by everything else to decide between incremental and full exports and to show the ✓ marker.
Per channel it records: the name, guild id and name, directory, when it was last exported, the lastMessageId cursor (the whole basis of incremental sync), the last message timestamp, the last thread id/name, thread and message counts, and a per-thread map with the same cursors (name, type, messageCount, lastMessageId, lastMessageTimestamp, lastAuthor) for every thread in the channel.
⚠️ Do not hand-edit the
statesection. It is a cache of what is actually on disk and is rebuilt byupdate-state— any manual edit will be overwritten by the next sync (which rewritesstateonly, leavingsettingsandguildsuntouched). If it ever looks wrong, rundiscord-sync update-state <dir>to rebuild it from the exports, anddiscord-sync validateto check it.
JSON Schema
A JSON Schema rendition of discord-sync.json is generated from the zod schema (the source of truth in src/lib/schema.ts) with:
discord-sync validate --emit-json-schema # writes discord-sync.schema.json
Point the file’s $schema property at discord-sync.schema.json to get editor tooltips and validation.
Default export parameters
Exports run through the tyrrrz/discordchatexporter:stable docker image with these settings (see export for the flags that override them where applicable):
| Parameter | Value | Notes |
|---|---|---|
| Format | Json | The only format the merge/HTML/Markdown steps understand |
| Threads | --include-threads All | Active and archived threads — essential for forum channels |
| Partitioning | --partition 10mb | Very large threads are split across several files |
| Timezone | --utc (+ TZ=UTC) | All timestamps are UTC |
| Parallelism | --parallel 1 | One download at a time |
Rate limiting
DiscordChatExporter is always invoked with --respect-rate-limits, so it honors Discord’s advisory rate-limit headers on its own, and --parallel 1 keeps it to a single request stream. On top of that, chunked exports pause between date windows (exportDelaySeconds plus 0..exportJitterSeconds seconds by default) and retry failed chunks with exponential backoff — chunking exists for resumability and for spreading very large first-time exports over time, not because DiscordChatExporter would otherwise hammer the API.
The asset downloader is similarly polite: assetDelayMs (~0.4s) plus jitter between downloads, honoring Retry-After on HTTP 429.