User Settings
/settings is the CardMirror editor's settings page, and a signed-in user's
choices there follow them across devices instead of staying in one browser's
localStorage.
- Route:
/settings(the panel itself renders in an isolated iframe at/settings/editor-panel) - Research agent settings: tabs of
/settingsitself, listed after the editor's under a Research agent heading in the same sidebar — one per section (account, language models, connectors, skills and memory, search sources, search settings, cloud storage, rewrite modes, voice). Each is linkable as/settings?category=research-<section>. The section list isresearch-agent-ui's own (research-agent-ui/settings); the panes load lazily, the first time one of those tabs is opened. The old/settings/researchand/settings/research/<section>pages now redirect to the matching tab (editor-<tab>sections to the editor's own tab). - Package:
debate-editor(the settings store,SETTING_METADATA, and the embedded panel builder),apps/debate-ai.com(lib/editor-preferences.ts,/api/settings,user_settingsD1 table)
What it shows
The editor's whole category set, listed in a searchable sidebar — General, Files, Appearance,
Accessibility, Editing, Keyboard, Comments & AI,
Collaboration — from debate-editor's exported tab list
(CARDMIRROR_SETTINGS_TABS, @debate/editor/settings-tabs) and rendered by its
CardMirrorSettingsSection (@debate/editor/settings-section), which wraps
buildEmbeddedSettingsPanel, so each tab is the same rows the editor's
gear-icon modal renders under it, minus the dialog chrome. General also
carries the personal dictionary, the performance benchmark and this install's
version info.
The page fills the whole window with no padding around it. The sidebar runs
its full height: the Settings title and a Back link sit at its top, and
the site's footer links (Docs, Github, Support, Privacy,
Reddit — the same FOOTER_LINKS list the app sidebar's footer reads) sit
at its bottom.
Appearance and Accessibility appear only here: they moved out of that modal when they moved to this page. Everything else is a second surface onto the same local store, so a change here shows up in the editor and the other way round.
EDITOR_SETTINGS_TABS (apps/debate-ai.com/lib/editor-preferences.ts) is the
one list of which categories those are, read both by the panel route that
renders them and by the API route that validates what they save. plugins is
deliberately not among them — every row in it is Electron-only, so the tab
would render empty on the web.
What it no longer shows
The page used to be this app's own account form, and that whole section is gone:
- the Debate style / Font size / Font family / Colour theme / Light-dark
pickers (
debate-round'sUserSettingsPanel) — the dock's gear-icon menu still carries the colour theme and light/dark rows. The whole form is back as the first tab of/settings, Preferences (stillUserSettingsPanel), ahead of the editor's own tabs;/settings/preferencesnow redirects to it - Favorite tools — a tool is starred from its own page or the command palette (the
/toolspage was removed), which is also where a renamed or removed tool now gets pruned from the saved list - Tool data — each tool page's header now carries a small badge (Saved to account / Syncing… / Not synced · Retry / Saved on this browser when signed out) for tools whose data syncs; see Tool Data Sync's Known gaps for what went with it (the sync itself is untouched and still runs app-wide)
- Word limit presets (
debate-round'sWordLimitPresetsPanel) — now editable directly on/word-count, in a collapsible "Manage word limit presets" section below the word-count form, rather than from here - Ebb Flow preferences — reachable with
Cmd/Ctrl+,inside a flow, where they apply - Default format for new speech documents (Files → New documents) — a
new speech document is now always created as
.cmir, CardMirror's native format, so autosave always applies to it (autosave only writes.cmir). Save As still exports a.docxfor teammates on Verbatim. The format isSPEECH_DOC_FORMATindebate-editor'ssrc/editor/speech-filename.ts
Their /api/settings fields (debateStyle, fontSize, wordLimitPresets, …)
are all still stored and served; what changed is that nothing writes them from
here any more.
Your own AI key
The Preferences tab has a Your own AI key box. Paste an
OpenRouter key (sk-or-…) or an
Anthropic key (sk-ant-…) and the AI features — card AI analysis, AI coach and
judge, citation and OCR fixes, every caller of /api/reason-ai and
/api/card-ai-analysis — run on your key instead of Debate AI's shared one.
You set your own spending limit on that key with OpenRouter (or Anthropic), and
Debate AI's daily AI request and card-analysis limits stop applying to you.
The same box appears:
- in the pricing dialog when you hit a daily AI limit, as an alternative to upgrading, and
- in an AI limit reached dialog when Debate AI's shared key itself has run
out of credit or hit its spending cap (the route answers
503with anx-ai-key-neededheader). That dialog is an upgrade box: it offers the plan cards to upgrade your account first, with bringing your own key as the other option. If the plans can't be loaded, only the key option shows.
The key is kept in this browser's localStorage only — never in your account's
settings row — and is sent, as the x-user-ai-key header, only on same-origin
requests to the two AI routes. The server uses it for that one request and
never stores or logs it. Remove it to go back to the shared key.
- Packages:
debate-webview(lib/ai/own-ai-key.ts,components/settings/OwnAiKeySection.tsx,components/pricing/OwnAiKeyDialog.tsx),apps/debate-ai.com(lib/ai/provider-key.ts)
Flow editor preferences follow your account
The Ebb flow editor's display preferences (Cmd/Ctrl+,) and keybinding
overrides sync to your account when you are signed in, through the same tool
data sync as the other tools (flowEditorDisplaySettings and
flowKeymap in debate-data-sync's catalog). What follows you:
flow font, default grid zoom, the editing toggles (insert-paste, append-edit,
scroll-zoom, align speeches, tooltips, Vim in the RFD pane), CardMirror
options, light/dark theme, side colours, and every keymap override.
What stays on the device it was set on: the flows folder, whether the sidebar
and RFD pane are open, and the live-collaboration switches, display name and
contacts. A pull from the account never resets those. Engineering notes:
debate-data-sync's state/flow-editor-settings-codec.ts — a field joins the
account only by being added to SYNCED_FLOW_DISPLAY_FIELDS.
Saved-to-account marker on flow tabs
Each flow tab in the sidebar's Open Tabs group shows a small cloud icon
once that flow has been saved to your account from the Flow History dialog
(including bulk saves): a plain cloud means the account copy matches what you
see, a crossed-out amber cloud means you've edited it since. A tab with no icon
has no baseline yet. The marker survives a reload: each save remembers the
version the account stamped on it, and opening the /debate start screen or a
round's flow tabs (or the Saved to account tab in Flow History) re-adopts
that baseline only if your account still holds exactly that version, so edits made elsewhere (or by a different account on the same
browser) never show as saved. Engineering notes: debate-round's
state/flowAccountStatus.ts; the on-mount restore is state/restoreAccountBaselines.ts
(one fetch per page session, silent when signed out).
The same cloud icon appears on the round cards of the /debate start screen
(Featured and Recent) once the round itself has been saved to your account from
the Flow History dialog, and flips to the amber crossed-out cloud when the round
is edited afterwards. Saving a round's flows does not by itself mark the round.
Auto-save for flows already on your account
Once a flow has been saved to your account, further edits to it are saved
automatically about five seconds after you stop typing, and the tab's cloud
icon flips back to saved. Flows you have never saved are never uploaded
automatically, and archived flows are skipped. Auto-save never overwrites: if
the account holds a newer copy (saved from another device) the flow stays
amber and you resolve it from Flow History. A failed save is retried only after
the flow changes again. Turn it off with Auto-save flows to my account on the
Preferences tab; it is on by default, applies immediately, and is stored in
this browser only (not synced to other devices). Turning it off discards any
save still waiting on the timer. Engineering notes: debate-round's
state/flowAutoSave.ts, mounted by useFlowAutoSave in OpenTabsGroup; the
preference lives in state/flowAutoSavePreference.ts.
Choose how far auto-save goes under Flow auto-save in Settings → Preferences:
- Flows already on my account (default) — the behavior above.
- All flows — also uploads flows you have never saved. If your account already holds a flow with that id, it is treated as a conflict and left alone.
- Off — nothing is saved automatically; use Save now or Flow History.
The choice applies immediately in this browser. When you are signed in it is
also saved to your account (PUT /api/settings { flowAutoSave }, the
user_settings.flow_auto_save column) and adopted on your other devices the
next time Settings loads. Engineering notes:
state/flowAutoSaveSettings.ts (debate:flow-auto-save in localStorage,
normalizeFlowAutoSavePatch/parseFlowAutoSave for the route), read at each
run by createFlowAutoSaver's getMode. Deploy must add the new nullable
column (schema.ts is the only source of truth).
Data flow
debate-editor/settings.ts (the store, SETTING_METADATA, SECRET_SETTING_KEYS)
debate-editor/settings-ui.ts (buildEmbeddedSettingsPanel)
app/settings/editor-panel/page.tsx (iframe document: category sidebar, hydrate, debounced push)
→ GET/PUT /api/settings { editorPreferences }
lib/editor-preferences.ts (EDITOR_SETTINGS_TABS, EDITOR_PREFERENCE_KEYS, validation)
components/settings/CardMirrorSettingsPanel.tsx (the iframe wrapper, self-sizing)The local store stays authoritative: the page hydrates from the account on
load (a 401 just means signed out), and every change writes localStorage
immediately and mirrors to the account on a 600ms debounce. A failed mirror
never undoes the local change.
Credentials stay in the browser
The mirrored keys are the hosted categories' settings minus the
credentials among them — the provider API keys and the translation key that
debate-editor's SECRET_SETTING_KEYS already keeps out of a settings
export, plus the self-hosted relay's bearer token. EDITOR_PREFERENCE_KEYS
excludes them, so they are refused on the way in (reported as unknown keys,
not silently dropped) and filtered on the way back out of a stored row.
apps/debate-ai.com/lib/__tests__/editor-preferences.test.ts pins that.
The push is also limited to the rows that actually rendered — each one tagged
data-setting-key by the editor — rather than to every key in a hosted
category. A row this host hides (desktop-only card sharing, say) would
otherwise be mirrored at its default and overwrite whatever the account holds
for it.
Flow editor preferences follow your account
Signed in, the Ebb flow editor's look-and-behaviour preferences (font, default
grid zoom, theme, Aff/Neg colours, the Vim/insert-paste/append-edit/scroll-zoom/
align-speeches/tooltip toggles, the CardMirror options and your custom
keybindings) are saved to your account as flowEditorSettings and applied the
next time you open a flow on any device. Anything specific to one device — the
flows folder, collaboration name and relay toggles, contacts, and panel layout —
stays in that browser. Signed out, nothing changes: settings stay in
localStorage.
The welcome dialog shows once per account
The "Welcome to Debate AI" dialog of key links appears the first time an
account signs in, and never again on any device. Showing it records
user_settings.welcome_dismissed_at through GET/POST /api/settings/welcome (FirstLoginWelcomeProvider in @debate/webview,
helpers in lib/first-login-welcome.ts). A localStorage copy of the flag
saves the request on browsers that already know, and a browser that showed
the dialog before the flag moved to the account records it there once. If the
account's answer cannot be read (offline, an error), the dialog stays hidden
rather than risk showing it twice. A second account signing in on a browser
that already showed it does not see it.
Privacy
The Privacy tab (/settings?category=privacy) is where you decide what
Debate AI may publish of your recordings and how you are named on them.
- Recording release. A few paragraphs explain what publishing means. Until you pick I agree, nothing you record is published; Don't publish my recordings is the default and withdraws an earlier agreement. Agreeing records the date and the version of the wording you agreed to, and if the wording changes you are asked again.
- What is published: Audio and video, or Audio only, which leaves your camera out.
- How you are credited: Anonymous contributor (the default, your name hidden), a Username you type in, or your Real name, either in full or as first name and last initial ("Ada L."). The tab previews how you will appear.
- Privacy policy: a link to
/legal/privacy. - Request deletion of your data: ask for everything (including your account) or only some of it (published recordings, saved speech audio, flows, documents, comments, account settings), with a box for suggested deletions such as links to specific recordings. Requests go to the site's admins as a notification, and your past requests and their status are listed under the form. Nothing is deleted automatically.
Engineering notes: the rules live in @debate/webview's
lib/privacy/recording-release.ts (mayPublishRecording, includeVideo,
publicCreditName, the validators and the release text). The choices are
stored as JSON in user_settings.recording_privacy through GET/PUT /api/settings/privacy; requests go to GET/POST /api/settings/privacy/deletion-requests and the data_deletion_requests
table, created on first use by apps/debate-ai.com/lib/privacy/store.ts,
which also exports getRecordingPrivacy(db, userId) for any server code that
publishes a recording.
Known gaps: the release text is a plain-language placeholder that has not had
legal review. No feature publishes user recordings yet, so the settings take
effect as publishing features read them. There is no admin screen for the
deletion queue yet; admins see the notification and work the
data_deletion_requests table by hand.
Known gaps
No optimistic-concurrency handling in general — two tabs editing the same
field at once has the last PUT win. favoriteTools no longer has this
problem: every code path that changes it (toggleFavorite/removeFavorite's
single add/remove op, and pruneUnknown's batch removeFavoriteTools op)
now resolves against the account's row as it stands at write time, not a
client-computed whole-list replace, so two tabs adding, removing or pruning
different entries at once can no longer drop each other's change. A plain
whole-list favoriteTools replace is still accepted by the route for a
caller that genuinely needs one, but nothing in the app sends one anymore.
recentTools (see Command Palette's
"Recent" section) joins it as an op-based field — each tool visit sends a
single recordRecentTool op, resolved the same read-then-write way — since
it writes far more often than any picker-style field here (every tool open,
not just an explicit Save) and two tabs opening different tools in quick
succession would otherwise race the same way favorites used to.
editorPreferences is a plain merge of whatever the page last pushed, so it
has the same last-write-wins property: the editor's settings open in two
tabs, changed in both, end at whichever debounce landed second. Every other
field (debateStyle, colorTheme, etc.) is a plain whole-value replace with
no such protection — debateStyle/fontSize/colorTheme/themeMode are
written from /settings' Preferences tab and the dock's Theme submenu, both of
which apply and (when signed in) sync a single field at a time rather than a
whole form Save, so this mostly shows up as "two tabs pick a different
Debate style within the same second."
wordLimitPresets (debate-round's WordLimitPresetsPanel, rendered from
/word-count rather than from this page — see "What it no longer shows"
above) is also op-based now: addPreset/updatePreset/removePreset each
send a single addWordLimitPreset/updateWordLimitPreset/
removeWordLimitPreset op, resolved against the account's current list at
write time instead of a client-computed whole-list replace, mirroring
favoriteTools's fix above. A duplicate name, an unknown preset, or an
invalid word limit is refused with a 400 rather than silently corrupting
the stored list. A plain whole-list wordLimitPresets replace is still
accepted by the route for a caller that genuinely needs one, but
useWordLimitPresets.ts no longer sends one.
outlineFilterPresets (debate-practice-drills's named Outline filter
presets bar — see Argument Tree Outline)
is op-based the same way: addPreset/removePreset each send a single
addOutlineFilterPreset/removeOutlineFilterPreset op, resolved against
the account's current list at write time instead of a client-computed
whole-list replace, mirroring wordLimitPresets's fix above. A duplicate
name, an at-capacity list, or an invalid filter is refused with a 400
rather than silently corrupting the stored list. A plain whole-list
outlineFilterPresets replace is still accepted by the route for a caller
that genuinely needs one, but useOutlineFilterPresets.ts no longer sends
one.
Pinned debates
The debates you pin to the start screen's Featured section follow your
account. Each pin is a { roundId, pinnedAt } record in the pinnedDebates
collection of the tool-record sync (debate-data-sync's
TOOL_RECORD_COLLECTIONS, stored in saved_tool_records), so it syncs like
every other tool store and appears under Settings → Account → Tool data
as "Pinned Debates". A round's id is its saved_rounds.client_id, which a
cloud save keeps, so the pin names the same round on every device; a pin whose
round isn't on this device yet is skipped until the round arrives. Browsers
that stored pins as a bare list of ids are upgraded to records the first time
the list is read.
Known gap: pins are capped at 12 per browser, and the merge on sign-in is a union, so a pin removed on one device while another is offline can reappear until that device syncs.
Flow editor shortcuts
Shortcuts you rebind in the flow editor sync to your account as the
flowKeymap collection (listed in Tool data as "Flow Editor Shortcuts"),
one record per rebound action. They are stored under ebb-keymap-settings and
adapted by flow-keymap-codec.ts in debate-data-sync.
Known gap: the flow editor reads shortcuts once at startup, so a rebinding made
on another device applies the next time the editor loads. Display preferences
(ebb-display-settings) are not synced: they include device-local values such
as the flows folder path.
Save status on each tool
A tool page whose data syncs shows a small badge in its header: Saved to your
account once everything the tool stores has reached your account, Saving to
your account… while recent changes are on their way, Syncing… during the
first merge after you sign in, and Saved on this device when you're signed
out. It is read from the same auto-sync that powers Tool data (see
getToolRecordCollectionSyncStatus in debate-data-sync), so it never starts a
second sync, and it refreshes every few seconds rather than instantly.
While changes are waiting to be saved, a Save now button appears next to the badge and pushes that tool's data to your account immediately instead of waiting for the next automatic save. If the save fails the button turns into Retry save and its tooltip says why. Starring a tool as a favorite uses the star at the end of the same header row.
The /debate start screen has its own header, so it shows the same badge (and
Save now) beside its New flow button; DebateFlowPage takes it as a
startScreenActions prop because debate-round can't import the webview's
component. Once a round or flow is open, the same badge sits at the start of the
speech controls bar above the workspace (roundActions, handed to
SpeechControlsTopBar as leadingActions).
The shared-cards sub-pages (Evidence Library, Argument Library and Card Revision
History) keep their data under the /research/cards hub rather than under their
own route, so their headers name the collections explicitly
(ToolPageHeader's syncCollections prop, resolved by resolveToolSyncKeys)
and show the badge too.
The video library at /videos (and each /videos/<category> view of it) shows the badge (and Save now) beside its
search bar; LecturesPage takes it as a headerActionsSlot prop because
debate-videos can't import the webview's component.
The /research/cards search workspace has no ToolPageHeader, so its route
renders a slim strip above the workspace holding the badge (and Save now) for
the Evidence Library collections.
Known gap: the open-flow tabs and the /doc header are custom too and don't
show the badge yet.
