---
title: "Settings and app lifetime"
description: "The versioned EditorSettings object every editor of an app shares: local storage, per-account sync, the settings UI, the settingsUi prop and themes."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/settings"
---
# Settings and app lifetime

> The versioned EditorSettings object every editor of an app shares: local storage, per-account sync, the settings UI, the settingsUi prop and themes.

## One settings object per app

`app.use(createEditor())` (`createEditor` from `@skaile/forge-editor/vue`; the Nuxt module does it in a client plugin) provides the context; it wraps the framework-free `createEditorContext()` from `/core`, whose `settings` is a store (`get`, `set`, `update`, `subscribe`). The settings are one versioned JSON object (`EditorSettings`, version 2) that every editor, panel and tree of the app shares. A simple editor and a preset without a feature never prune the settings of features it does not install; unknown-to-this-bundle values stay in the stored JSON. `createEditor()` defaults the app's notifier to a queued Vue notifier; pass `notify` to replace it. The object has the sections `editing`, `ai`, `blocks` and `appearance`. Read and edit it with `useEditorSettings()` (`/vue` or `/react`); in features use the `ctx.settings` store. Validate with `parseEditorSettings` (drops unknown keys, replaces invalid values) or `editorSettingsSchema`.

## React

`<EditorContextProvider context={createEditorContext({ notify: reactNotifier })}>` (`EditorContextProvider`, `reactNotifier` and `Toaster` from `@skaile/forge-editor/react`, `createEditorContext` from `/core`) provides the same context to a React tree. The host creates it and owns it: the provider never disposes it, call `context.dispose()` when it outlives the app. `useEditorSettings()` returns `{ settings, update, set, ready, synced }`. Without `notify` the context drops app-level notices (`silentNotifier`). `settings` is the current `EditorSettings`, `update(draft => { … })` mutates a copy and stores it, and `set(value)` replaces it. `useStore(store)` subscribes a component to any `ReadonlyStore` of the core.

## Local storage and account sync

The settings are read from `localStorage` (`tiptap-editor:settings`) at install time and written back on every change; unreadable or blocked storage falls back to the defaults for the session. A record saved by an earlier version is migrated when it is read (`migrateEditorSettings`, applied by `parseEditorSettings`) and written back once: version 2 renamed keys (see the [migration guide](https://skaile-ai.github.io/forge-editor/guides/migration#persisted-names)). The server migrates a stored older record on `GET /api/editor/settings` and stores it again, and `PUT` accepts a body of an older version (a tab that was open during the update).

A backend that offers `settings` (the default backend and `createHttpBackend()` do; `createMemoryBackend()` keeps settings in memory only) syncs per account. On the first `useEditorSettings()` the client calls `backend.settings.load()` (the HTTP backend: `GET /api/editor/settings`; only once an editor-side component needs the settings, not at install time): the stored settings replace the local values (`synced`), `null` (`204`: the adapter keeps none), a refusal (`401`, `501`) and network errors keep the local ones; an account without stored settings uploads this browser's. Afterwards every change is sent with `backend.settings.save()` (debounced, 500 ms). The status codes are listed in [Server routes and authentication](https://skaile-ai.github.io/forge-editor/guides/routes-and-auth#settings-route-status-codes). A plain-Vue or React app gets this from the default backend only when its server implements the same two endpoints (`examples/react-advanced` reaches the Nuxt host's through its proxy), otherwise it passes `createMemoryBackend()` and stays local.

## View state of the tree

The tree's filters and expanded keys are the framework-free `TreeViewModel` of the app's context (`treeView(context)`); `useTreeView()` of `/tree/vue` and `/tree/react` returns them. Filters persist (`tip-tree:node-filters`), expansion lasts for the session. The model belongs to the app context (cached per context, nothing is global to the process), and a model that is dropped takes its subscription with it.

## Settings UI

Tabs come from the features of a preset (`composites` `editing`, `ai`, `authors`, `comments`, `themes` `appearance`); the simple editor has none. `EditorSettingsPanel` (props `editor`, `contentClass?`, `v-model:tab`) places the tabs anywhere; `EditorSettingsModal` (props `editor`, `v-model:open`, `v-model:tab`) is the same panel in a fixed-width (`42rem`) popup with a blurred backdrop. Both render nothing when the editor's preset has no tab.

The `settingsUi` prop of `EditorSurface` and `AdvancedEditor` takes two values: `'internal'` (default) mounts the modal inside the surface and the toolbar's settings entry opens it. `'external'` mounts nothing and emits `settingsOpen(tab?)`; the host then shows its own `EditorSettingsPanel` or `EditorSettingsModal` for the same `editor`:

```vue
<script setup lang="ts">
import { EditorSettingsModal } from '@skaile/forge-editor/vue'
import { AdvancedEditor } from '@skaile/forge-editor/vue/advanced'
import type { Editor } from '@tiptap/vue-3'
import { ref, shallowRef } from 'vue'

const editor = shallowRef<Editor>()
const open = ref(false)
const tab = ref<string>()
</script>

<template>
  <AdvancedEditor settings-ui="external" @ready="editor = $event" @settings-open="(t) => { tab = t; open = true }" />
  <EditorSettingsModal v-model:open="open" v-model:tab="tab" :editor="editor" />
</template>
```

## Themes

`themesFeature()` (part of `advancedEditor()` on both surfaces): the setting `appearance.theme` (`null` = Default appearance, which applies no Theme) names an Obsidian theme served by `GET /api/editor/themes/:id`; users pick it in the settings tab „Theme“. The feature's `setup` hook starts one runtime per app as soon as the first surface mounts (reference-counted, released by the teardown: on Vue it lives until the Vue app unmounts, on React until the last surface that started it unmounts), injects the scoped CSS as `<style data-tiptap-theme>` and sets `obsidian-theme theme-dark|theme-light` on `<html>` (following `html.dark`/`html.light`, else the system preference). `styles/themes.css` bridges Obsidian variables onto every `--ui-*` token (React: `react/styles/themes-bridge.css` onto the shadcn tokens), so the **whole site** is themed, not only the editor. Needs an adapter with `listThemes`/`loadTheme`. Builtin themes in this repository (`server/themes/<id>/`): `nord`, `things`, `minimal`, `catppuccin`, `border`, `blue-topaz`, `tokyo-night`. Details, mapping table and limits: [Themes](https://skaile-ai.github.io/forge-editor/guides/themes).


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
