---
title: "Pitfalls and invariants"
description: "Rules that must stay true when integrating or changing @skaile/forge-editor: CSS order, mandatory context, SSR, mount-time props, bundle isolation, entries."
canonical_url: "https://skaile-ai.github.io/forge-editor/ai-agents/invariants"
---
# Pitfalls and invariants

> Rules that must stay true when integrating or changing @skaile/forge-editor: CSS order, mandatory context, SSR, mount-time props, bundle isolation, entries.

Check this list before and after every change to an integration. Each bullet is a failure that does not announce itself at compile time.

- **CSS order.** `core.css` after Tailwind and Nuxt UI, feature sheets after it, `themes.css` last, and `@source` so Tailwind sees the library. The exact order per host is in [CSS and UI-provider requirements](https://skaile-ai.github.io/forge-editor/reference/css-and-ui).
- **Editor context is mandatory.** `EditorSurface`, `SimpleEditor`, `AdvancedEditor` and `EditorToolbar` throw without `app.use(createEditor())` (the Nuxt module does this in its client plugin); the React components throw without `<EditorContextProvider context={createEditorContext()}>`, and the React host owns (disposes) that context.
- **Notices.** Features send user-facing messages through `ctx.notify` / the context's `notify` (`Notifier`, `silentNotifier` drops them). Vue: the queued notifier hands them to Nuxt UI toasts once a surface mounted (needs `<UApp>`). React: pass `createEditorContext({ notify: reactNotifier })` and mount sonner's `<Toaster />` (exported by `/react`) once; without it no notice is visible.
- **UI provider.** Wrap the page in `<UApp>`; plain Vue also needs `app.use(ui)` (`@nuxt/ui/vue-plugin`) and the `ui({ router: false })` Vite plugin.
- **SSR.** Editors are client-only. Nuxt apps in this repository run `ssr: false`; with SSR enabled wrap editors in `<ClientOnly>`.
- **Mount-time props.** The preset (schema, features), `collaboration`, `abilities`, `extensions`, `commands`, `blockActions`, `blockTypes`, `blockUi`, `coreContent`, `documentId` are read once at setup. Change them by remounting with a new `key`. `editable`, `metadata` and the model stay reactive. Build presets in `<script setup>` (Vue) or a component/`useMemo` (React), not on every render.
- **Stable props.** Pass `metadata`, `abilities` and `extensions` as constants; a new object per render replaces the lists each time.
- **Schema match.** A simple editor rejects advanced JSON through the `contentError` event (the last valid document stays). `collaboration` needs a preset containing `collab`; `commentsFeature()` needs `collab`.
- **Bundle isolation.** `core`/`settings` import no feature; a feature imports `core`, `settings` and its own `schema`/`protocol`, never another feature; integrations import only the features they connect; the `simple` bundle contains no feature code, Yjs, AI SDK, Nuxt or server modules. Keep it that way when adding code, and prove it with `check:editor-entries`. Rules: [Dependency rules](https://skaile-ai.github.io/forge-editor/reference/dependency-rules).
- **Entries.** Server code imports only framework-free entries (`/schema`, `/protocol`, `/types`, `/settings`) or `/server`. A new public entry needs an explicit `package.json#exports` row; there is no wildcard.
- **Icons.** `i-lucide-*` names resolve at build time inside the Vite root; a workspace library lies outside it. Bundle them (see `examples/vue-simple/vite.config.ts`) to avoid Iconify API requests at runtime.
- **React hosts.** CSS: Tailwind, `tw-animate-css`, then either `.../react/styles/ui-vars.css`, `core.css` and the feature sheets, or for the advanced editor the single `.../react/styles/advanced.css`; the shadcn theme block, and `@source` to the whole `dist` of the package (`node_modules/@skaile/forge-editor/dist`; `packages/editor/src` in this repository), never just `react`: the Features' components are in `features/*/react`; icons: `addCollection(lucide)` from `@iconify/react` before rendering; `<Toaster />` for notices. The preset, `metadata`, `abilities`, `extensions` and `commands` are mount-time: keep them module constants (new objects per render replace the lists). `editor` from `onReady` goes in `useState`, and `EditorToolbar` takes it as `editor`. Examples: [Quick start: React](https://skaile-ai.github.io/forge-editor/getting-started/quick-start-react) and [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react).
- **Settings keys.** Only keys of `EditorSettings` persist; unknown ones are dropped on read and write.
- **Design tokens.** `@skaile/forge-editor/styles/tokens.css` declares the shared `--sk-*` variables (`--sk-bg`, `--sk-fg`, `--sk-muted`, `--sk-border`, `--sk-primary`, `--sk-primary-fg`, `--sk-success`, `--sk-warning`, `--sk-danger`, `--sk-radius`, `--sk-font`) under `:where(:root)` (dark values under `:where(.dark)`). Later UI work in this package reads `--sk-*`; map them once in the host.
- **Module order.** `@nuxt/ui` before `@skaile/forge-editor/nuxt` in `modules`.
- **Adapters tier.** Entries under `adapters/`, `anchors`, `comments/{server,json,drizzle,client}` and `server/nitro` supply storage and wiring; they import no application code. Vue is reachable only from `./adapters/vue`, React only from `./adapters/react` (and the framework layers), Nitro (`h3`) only from `./server/nitro`, `drizzle-orm` only from the two drizzle entries; `./adapters` is types only. `./server/nest` and `./adapters/prisma` are reserved and not exported. The Nitro host and the HTTP backend (`createHttpBackend`, which `useEditorDocument` reads documents, collaboration and comments through) speak one wire contract: `GET`/`PUT /api/editor/documents/:id` (TipTap JSON, 404 = missing, `PUT` 204 and only for adapters that manage documents), `GET`/`POST /api/editor/comments/:id` (`handleCommentsRequest`, which trusts its caller), `/collab/:id` (Hocuspocus). Every route authenticates through `ports.identity.authenticate({ headers, token })`; `null` answers 401; the default outside production is an anonymous dev user, in production nobody. The JSON and forge adapters and the JSON comment store are single-node. Mount the surface with `:key="id"` and `v-if="doc.ready.value"` (React: `key={id}` and `doc.ready`), bind with `v-bind="doc.bind.value"` (React: `{...doc.bind}`). Runnable reference: `examples/vue-nitro-json` (`bun run proto:vue-nitro-json`, port 3046).


## Sitemap

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