Documentation, Contributing, and Config UX Research
Research and design analysis. This document proposes a direction and a phased plan; it does not change code. It builds on Configuration Organization Research, which owns option naming and the flat-versus-nested convention, and does not re-litigate those.
Executive Summary
Current State
The contributing experience for teams-for-linux is mature, yet the project carries one structural weakness in its configuration documentation. The application exposes 76 options defined in a single yargs block in app/config/index.js, each already carrying its own description, type, and default value, but those definitions live in isolation from the separate 924-line configuration.md reference. This manual duplication is a maintenance burden that drifts, a problem confirmed during this research by a stale protocol-handler regex default and two config keys that exist in code but are absent from the docs. The project's IPC surface already demonstrates the better model: it is auto-generated from code and ships with a clean guide for adding channels. The configuration layer has none of that synchronization, and there is no in-app editor, so users hand-edit a config file across three platform-specific paths.
The documentation platform, Docusaurus 3.10, is healthy and is not the source of the inconsistency, so the solution is an architectural change rather than a tooling upgrade. The fix is to establish the code as the single source of truth that feeds the documentation, a future settings UI, and startup validation, which removes the manual effort of keeping separate copies in sync and prevents the recurrence of stale defaults or missing keys.
Key Findings
The findings below are grounded in the code as it stands today; file and line references are in the Detailed Analysis section.
- There are 76 config options, each with
default,describe, and (75 of 76)type; 5 carrychoicesand 6 carrydeprecatedmetadata. This is uniform and harvestable. configuration.md(924 lines, 60 headings) mirrors these by hand with no codegen link and no "keep in sync" banner. The IPC docs, by contrast, are generated byscripts/generateIpcDocs.jsand carry a "do not edit" banner.- Confirmed drift: the documented
msTeamsProtocolsdefault is the old narrow regex while the code default is far broader;mqtt.homeAssistant.*andauth.webauthn.debugexist in code but are undocumented. AppConfigurationholds three stores: an immutable-intendedstartupConfig(the runtime source of truth, actually mutated in place by six menu toggles), a persistentsettingsStore, and alegacyConfigStorethat already layers six persisted overrides on top of file config at boot.- The only general way to apply a config change is a full restart (
app.relaunch); the six menu toggles are the one proven live-update path, pushed to the renderer via aconfig-changeddelta. - No in-app editor exists for the 76 wrapper options;
settings.jsonly manipulates Teams' own web settings (theme, chat density). - The platform is already Docusaurus 3.10 (not 3.9), with offline local search, MDX 3 + React 19, and Mermaid, deployed statically to GitHub Pages.
Approach Decision
The central thesis is to make the configuration definition in app/config/index.js the single source of truth for one extractable schema, and to treat three things as consumers of it rather than three hand-maintained copies: the documentation reference, an in-app settings UI, and startup validation. The existing option block is already enough to kill documentation drift and power a docs config explorer immediately, because every option carries key, type, default, description, choices, and deprecation. It is not yet enough to drive an in-app editor safely, for two reasons: the schema encodes nothing about whether an option applies live or needs a restart, and the 17 object-typed options describe only their top-level key, leaving nested fields opaque.
So documentation comes first and is low risk, the in-app UI is the reward for first investing in two new schema dimensions (an apply-mode field and nested-field metadata), and restart-required options can ship earlier by reusing the app's existing relaunch path. Drift dies in the first generation phase; the UI is the payoff for the schema investment, not a free side effect.
Expected Benefits
Eliminating the hand-maintained mirror removes an entire class of bug (stale or missing documented options) at its root and reuses a codegen pattern the team already trusts. The same generated schema then unlocks an interactive config explorer in the docs and, later, an in-app settings window that removes the hand-edit-across-three-paths friction. A single yargs edit would propagate to the reference docs, the explorer, the in-app UI, and runtime validation with no hand-sync anywhere.
Risk Level
Low to medium overall. Phase 0 and Phase 1 (the drift fixes and the generator) are low risk and reuse the generateIpcDocs.js precedent. The in-app UI (Phase 3) is the high-risk surface because the renderer would write config under disabled contextIsolation/sandbox, so it is gated behind explicit schema metadata and mandatory validation.
Detailed Analysis
The configuration system today
Config is loaded once at startup by merging a system config.json under the user's config.json (user wins) into a yargs instance over the 76 option definitions, then stored as a single object in the AppConfiguration constructor. That startupConfig is the single runtime source of truth, read directly as a plain object everywhere and returned by the get-config IPC handler; it is intended immutable but is not frozen, and six menu toggles mutate it in place. The only general way to apply a change is a full restart: watchConfigFile emits config-file-changed and restartApp calls app.relaunch plus app.exit. The one proven live-update path is those six notification/badge toggles, which persist to legacyConfigStore and push a config-changed delta the preload merges into the local config.
Three stores coexist. startupConfig is the merged runtime object. settingsStore (electron-store, name settings) holds app-owned persistent state that is not among the 76 options (acknowledged warnings, partitions, multi-account profiles) and is the natural home for future user-edited settings. legacyConfigStore (name config) persists six menu-toggle overrides and copies present keys over startupConfig at boot so they win, which is exactly the persisted-override-on-top-of-file precedence pattern an in-app editor should reuse. Every option already carries default, describe, and type inline, with choices on five and deprecated on six, which is the harvestable metadata the whole thesis depends on. The relevant evidence is in app/config/index.js (option defs and getDeprecatedOptions), app/appConfiguration/index.js, app/index.js (boot, watch, relaunch), and app/menus/index.js (toggle persistence).
The security model constrains any renderer-driven config write. Every IPC channel must appear in the allowedChannels allowlist in app/security/ipcValidator.js or it is blocked, and validateIpcChannel runs a payload sanitizer that strips prototype-pollution keys. Because contextIsolation and sandbox are disabled for Teams DOM access, that allowlist plus the sanitizer are the only compensating controls, so any config-write channel needs a tightly scoped allowlist entry, value validation modeled on settings.js validateSettingsInput, and a generate-ipc-docs run.
Documentation and contributing experience
The onboarding is unusually mature for a project this size, with layered contributing guides, a module index that carries a create-a-module checklist, ADRs, a research index, and solid PR and issue templates. The IPC docs are the model to copy: ipc-api.md states the channel list is auto-generated, links to ipc-api-generated.md, and provides a four-step add-a-channel walkthrough. The one structural weakness is that the 76 config options are mirrored by hand with no codegen link and no source-of-truth banner, and there is no add-a-config-option guide to match the add-an-IPC-channel one, so a contributor adding an option must reverse-engineer the yargs entry, the nested-from-day-one convention, the AppConfiguration wiring, and the manual doc edit.
The drift this produces is not hypothetical. The documented msTeamsProtocols default is the old narrow regex (configuration.md line 394) while the code default (app/config/index.js lines 496-504) is far broader and adds live.com and cloud.microsoft. The mqtt.homeAssistant.{enabled,discoveryPrefix,deviceName} object and the auth.webauthn.debug flag exist in code with defaults and describe text but have no rows in the docs. There are also two small polish items: two [!NOTE] blocks in configuration.md use GitHub alert syntax without the leading blockquote marker and should be Docusaurus :::note admonitions per the project's own Markdown Standards, and CLAUDE.md plus the index point to a quick-reference.md that does not exist.
In-app settings UI feasibility
There is no editor for the wrapper options today, but a dedicated settings window is feasible by copying the existing secondary-window pattern already used for the join-meeting dialog, mounted from the Settings menu and the tray, reading the schema to render each option with the right control. The honest constraint is apply-mode: most options are consumed at boot and need a restart (for example disableGpu, electronCLIFlags, partition, frame, proxyServer, chromeUserAgent), while only the six toggles plus Teams theme/density are live. A trustworthy UI must therefore mark restart-required options and trigger the existing relaunch on save, and persist edits to a store layered over startupConfig at boot exactly like legacyConfigStore does, rather than pretending runtime config is freely mutable.
The docs platform
Docusaurus 3.10 already provides offline local search (via @easyops-cn/docusaurus-search-local, no external service), MDX 3 with React 19, Mermaid diagrams, dark mode, edit-this-page links, and a working static GitHub Pages build with onBrokenLinks: 'throw' as a quality gate. The platform is not the bottleneck. The real gain is an interactive in-browser config explorer fed by the generated schema, which MDX and React already make possible with no platform change. Versioned docs are a poor fit because release-please ships often and the app auto-updates, so nearly all readers run latest, and migrating to Starlight or Nextra would re-solve already-solved problems (search, deploy, Mermaid) for no user-visible win.
Proposed Solution
The plan front-loads correctness and the generator, then treats every later surface as a consumer of the generated schema. Phases 0 through 2 deliver value with low risk and no breaking changes; Phase 3 is gated behind schema metadata the project does not have yet.
Phase 0 — Zero-breaking-change correctness and hygiene (effort S, risk near-zero)
Fix the three confirmed drift items in configuration.md (the msTeamsProtocols default, the mqtt.homeAssistant.* rows, the auth.webauthn.debug row); fix the two broken [!NOTE] admonitions to :::note; resolve the dangling quick-reference.md reference (create it or remove the pointers); add a "Status / scope" banner to configuration-organization-research.md clarifying that its Phases 2-3 auto-migration is deferred, and collapse the large deferred code blocks behind <details>; and add an "Adding a Configuration Option" walkthrough to contributing.md mirroring the IPC guide. Unlocks immediate correctness and the contributor on-ramp that Phase 1 later automates.
Phase 1 — Schema as source of truth + generated config reference (effort M, risk medium)
Add scripts/generateConfigDocs.js modeled on generateIpcDocs.js (same "do not edit / auto-generated" banner), preferring to require the config module and read parsed yargs metadata over brittle regex against the 681-line inline chain. This needs a small prerequisite refactor: app/config/index.js exports only argv today, and evaluating it has side effects (logger init, file watchers, process.argv parsing), so it must additionally export the raw option-definition object (or an extractYargConfig helper) that the generator can read without triggering those side effects. Emit two artifacts from one pass: a generated configuration-generated.md reference grouped by the existing category headings, and a machine-readable config-schema.json that downstream consumers import. Refactor configuration.md so curated prose (Quick Start, usage examples, platform notes) stays hand-written and only the reference tables come from the generated file. Add a generate-config-docs --check drift guard wired into CI, warn-only first then hard-fail. Normalize runtime-expression defaults (those derived from the running Chrome version or the current user) and long regex defaults to a stable documented form so the output is not machine-specific; for example chromeUserAgent embeds process.versions.chrome, which is undefined outside Electron, so the generator must fall back to a static placeholder rather than emit Chrome/undefined. This is the keystone: drift dies here, and config-schema.json is the single artifact Phases 2 and 3 both consume.
Phase 2 — Interactive config explorer in the docs site (effort M, risk medium)
A React/MDX component in docs-site/src, fed by config-schema.json: a searchable, filterable table of all 76 options with type/default/choices/deprecated badges, per-option "copy as JSON", and a "build my config.json" basket that assembles a valid file. No platform change is needed. Ship read, filter, and copy first; defer dependency-aware validation (the customNotification/graphApi/mqtt/auth gating from the config-organization research) until users ask, to avoid scope creep into a full wizard.
Phase 3a — Schema metadata expansion, the UI prerequisite (effort M, risk medium)
Add an applyMode dimension to each option (live versus restart), defaulting every un-audited option to restart (safe, reuses relaunch) and only promoting to live with a verified runtime re-read path. Expand metadata for the 17 object-typed options so nested leaves carry their own describe/type/default. Prefer an inline sibling field in the yargs objects so the Phase 1 generator harvests it in the same pass. This is the missing dimension that makes an in-app editor honest about what takes effect now versus after restart.
Phase 3b — In-app settings window (effort L, risk high)
A dedicated, sandboxed settings BrowserWindow mounted from the Settings submenu and tray, copying the join-meeting dialog pattern. The renderer reads the schema to render each option with the correct control and a clear "requires restart" marker where applicable. A single tightly scoped config-write IPC channel is added to the ipcValidator allowlist with mandatory value validation modeled on validateSettingsInput, followed by a generate-ipc-docs run; no generic "write arbitrary config" channel. Because contextIsolation is disabled, the write handler must also perform deep-write sanitization in its own right, explicitly rejecting __proto__, constructor, and prototype at every nesting level independent of the global ipcValidator sanitizer (defense in depth), since nested object writes are otherwise a prototype-pollution vector. Edits persist to a dedicated override store layered over startupConfig at boot (the legacyConfigStore pattern); restart-required options relaunch, while the live subset reuses the config-changed delta path. Ship restart-required options first, since they only need write plus relaunch.
Phase 4 — Runtime validation and contributor loop closure (effort M, risk medium)
Use config-schema.json to validate config.json at startup (type, choices, deprecation), warn-only first and never hard-failing on unknown keys a future version might own, keeping validation at the boundary. Wire deprecation so flagged options warn at startup and render a deprecated badge in both the explorer and the UI from one source. Finalize the "Adding a Configuration Option" guide to its end state (add the yargs entry, run the generator, done) and update roadmap.md. This completes the one-schema-three-consumers thesis.
Ambitious ideas worth considering
Three ideas go beyond the baseline plan and are grounded in this codebase rather than bolted on.
The first is self-documenting config diagnostics: a "copy effective config" action (in the settings window and as a CLI flag) that emits the resolved startupConfig with every value annotated by its source (built-in default, system config, user config, or persisted override), using the same schema. The three-store precedence is invisible today and is a recurring support-friction source, and this turns the merge logic a maintainer has to mentally trace into a one-click artifact that dovetails with the bug template's existing diagnostics ask.
The second is a schema-driven config migration codemod that explicitly does not ship as runtime auto-migration (which the prior research rejected for good reasons), but as a user-invoked teams-for-linux --migrate-config that rewrites a config from deprecated flat keys to the nested form and prints a diff. It captures the cleanup value of the deprecation metadata without the runtime risk, and it is generated from the same schema so it never drifts.
The third is a config-option lint that runs in CI on every PR touching app/config/index.js and fails if a new option lacks a describe, a type, or an applyMode, the same way the IPC workflow enforces the allowlist. The thesis collapses the moment one contributor adds an option without metadata, so making schema completeness a merge gate keeps the single source of truth structurally self-enforcing rather than dependent on reviewer vigilance.
What we deliberately will not do (yet)
Versioned docs are not worth it while release-please ships frequently and the app auto-updates, because nearly all readers run latest and versioning would multiply the maintenance surface for marginal benefit. Migrating the docs platform to Starlight or Nextra re-solves problems Docusaurus 3.10 already handles and discards the working local-search and v4-compat setup for no user-visible win. A full config wizard with conditional dependency validation is real eventual value but a scope-creep trap, so the explorer ships read/filter/copy first. Flipping Docusaurus fasterByDefault on now would force rewriting HTML comments to JSX across existing pages for no reader benefit. Runtime in-process auto-migration of config.json was already rejected, and its value is better captured by the opt-in codemod above.
Risks and Mitigations
| Risk | Mitigation |
|---|---|
Schema lacks an applyMode dimension, so a UI cannot honestly tell live from restart-required, producing silent no-ops. | Treat applyMode as a hard prerequisite (Phase 3a); default un-audited options to restart; promote to live only with a verified re-read path. |
| The 17 object-typed options describe only the top level, so nested leaves render thin or are omitted (the drift already found). | Expand nested-leaf metadata as an explicit deliverable; accept thin object rows in docs short-term but do not ship the UI for an object option until its leaves are described. |
Renderer config writes under disabled contextIsolation/sandbox widen the attack surface. | One tightly scoped config-write channel in the allowlist, mandatory validation, reject any key not in the schema, run generate-ipc-docs. |
Runtime-expression and long-regex defaults serialize into machine-specific or malformed markdown and break the onBrokenLinks: 'throw' gate. | Normalize such defaults to a stable documented form in the generator; keep curated prose hand-written; run the drift check warn-only first. |
Mutating startupConfig in place conflicts with the "immutable after startup" rule and a UI amplifies it. | Adopt the legacyConfigStore model explicitly: persist edits to a store and layer them over startupConfig at boot. |
References
- Configuration Organization Research — option naming, flat-versus-nested, conditional dependencies (Phase 1 done, Phases 2-3 deferred).
- Graph API Integration Research — references a deferred settings UI.
app/config/index.js,app/config/defaults.js— the 76 option definitions.app/appConfiguration/index.js— the three stores.app/security/ipcValidator.js— the IPC allowlist and payload sanitizer.scripts/generateIpcDocs.js— the codegen precedent to mirror.docs-site/docs/configuration.md— the hand-maintained reference to generate.