diff --git a/website/src/routes/(docs)/preact/guides/(advanced-guides)/architecture/index.mdx b/website/src/routes/(docs)/preact/guides/(advanced-guides)/architecture/index.mdx
new file mode 100644
index 00000000..5706ad12
--- /dev/null
+++ b/website/src/routes/(docs)/preact/guides/(advanced-guides)/architecture/index.mdx
@@ -0,0 +1,137 @@
+---
+title: Architecture
+description: >-
+ A look behind the scenes at how Formisch is structured. Learn how the core,
+ methods and framework packages fit together, how a single signal interface
+ plugs into each framework's native reactivity, and what happens when you
+ call `useForm`.
+contributors:
+ - fabian-hiller
+---
+
+import { Link } from '~/components';
+
+# Architecture
+
+You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support Preact, SolidJS, React, Vue, Svelte and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.
+
+## Three packages, three responsibilities
+
+Formisch is structured as three packages, each with a clear role:
+
+- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
+- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and so on. Each method lives in its own file and only imports the core helpers it actually needs.
+- [`@formisch/preact`](https://github.com/open-circle/formisch/tree/main/frameworks/preact) is the thin user-facing layer. It provides the `useForm`, `useField` and `useFieldArray` hooks, the `
`, `` and `` components, and re-exports every method from `@formisch/methods` so you only import from a single package.
+
+```
+@formisch/preact
+ ├─ useForm, , , … (Preact-specific reactive layer)
+ └─ re-exports @formisch/methods/preact (focus, reset, validate, …)
+ └─ @formisch/core/preact (createFormStore, types, signals)
+ └─ framework adapter (createSignal, batch, untrack, createId)
+```
+
+The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `useForm`, `` and `` ships nothing else, even though `@formisch/preact` re-exports every method.
+
+## One core, one adapter per framework
+
+The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/preact`, `@formisch/core/solid`, `@formisch/core/react`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.
+
+The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.preact.ts` (or `.solid.ts`, `.react.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.
+
+Preact already has a native signal primitive in `@preact/signals`, so [the Preact adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.preact.ts) is essentially a re-export:
+
+```ts
+export {
+ signal as createSignal,
+ untracked as untrack,
+ batch,
+} from '@preact/signals';
+```
+
+The `Signal` shape that Formisch's core relies on — a `.value` getter that tracks and a `.value` setter that notifies — is exactly what `@preact/signals` already provides. For frameworks that don't have native signals (like React), the adapter implements a small pub/sub system from scratch that exposes the same shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.
+
+This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in real `@preact/signals` instances, with full access to their automatic dependency tracking and batching.
+
+That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Svelte user is a bug fixed for Preact, Solid, React and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.
+
+## State lives in signals
+
+Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:
+
+```ts
+interface Signal {
+ get value(): T;
+ set value(nextValue: T): void;
+}
+```
+
+Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal — typically a single `` — re-render.
+
+This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.
+
+## What happens when you call `useForm`
+
+When you call `useForm`, three layers cooperate to produce the form store you receive.
+
+```tsx
+import { useForm } from '@formisch/preact';
+import * as v from 'valibot';
+
+const LoginSchema = v.object({
+ email: v.pipe(v.string(), v.email()),
+ password: v.pipe(v.string(), v.minLength(8)),
+});
+
+function LoginForm() {
+ const loginForm = useForm({ schema: LoginSchema });
+ // …
+}
+```
+
+First, the [Preact wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/preact/src/hooks/useForm/useForm.ts) memoizes the whole form construction inside a single `useMemo` with an empty dependency array, so the form is built exactly once per component instance. Inside that callback, [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) is called with the configuration and a parse closure that captures your schema, then the public form store is assembled on top of the internal store it returns:
+
+```ts
+const form = useMemo(() => {
+ const internalFormStore = createFormStore(config, (input) =>
+ v.safeParseAsync(config.schema, input)
+ );
+ return {
+ [INTERNAL]: internalFormStore,
+ isSubmitting: internalFormStore.isSubmitting,
+ isSubmitted: internalFormStore.isSubmitted,
+ isValidating: internalFormStore.isValidating,
+ isTouched: computed(() => getFieldBool(internalFormStore, 'isTouched')),
+ isDirty: computed(() => getFieldBool(internalFormStore, 'isDirty')),
+ isValid: computed(() => !getFieldBool(internalFormStore, 'errors')),
+ errors: internalFormStore.errors,
+ };
+}, []);
+```
+
+The interesting work happens inside `createFormStore`. It calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:
+
+- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
+- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
+- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
+- Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into.
+- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.
+
+At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.
+
+The form object the wrapper returns sits on top of that internal store. Form-level signals are exposed directly, while derived state like `isTouched`, `isDirty` and `isValid` is wrapped in `computed` so it recomputes lazily as the underlying field signals change.
+
+The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.
+
+## Why this design pays off
+
+The same four properties keep coming back, and now you can see where they come from:
+
+- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/preact`](https://github.com/open-circle/formisch/tree/main/frameworks/preact), a form that uses only `useForm` and `` never pulls in `validate`, `reset`, `insert` or the other operations. Tree-shaking gets to do real work.
+- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly and `@preact/signals` handles propagation. Updating one field does not invalidate the rest.
+- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
+- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.
+
+## Further reading
+
+For a closer look at how Valibot's inference flows into your editor, see TypeScript. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
diff --git a/website/src/routes/(docs)/preact/guides/menu.md b/website/src/routes/(docs)/preact/guides/menu.md
index 648f1891..b359313d 100644
--- a/website/src/routes/(docs)/preact/guides/menu.md
+++ b/website/src/routes/(docs)/preact/guides/menu.md
@@ -22,3 +22,4 @@
- [Nested fields](/preact/guides/nested-fields/)
- [Field arrays](/preact/guides/field-arrays/)
- [TypeScript](/preact/guides/typescript/)
+- [Architecture](/preact/guides/architecture/)
diff --git a/website/src/routes/(docs)/qwik/guides/(advanced-guides)/architecture/index.mdx b/website/src/routes/(docs)/qwik/guides/(advanced-guides)/architecture/index.mdx
new file mode 100644
index 00000000..08cd7b62
--- /dev/null
+++ b/website/src/routes/(docs)/qwik/guides/(advanced-guides)/architecture/index.mdx
@@ -0,0 +1,144 @@
+---
+title: Architecture
+description: >-
+ A look behind the scenes at how Formisch is structured. Learn how the core,
+ methods and framework packages fit together, how a single signal interface
+ plugs into each framework's native reactivity, and what happens when you
+ call `useForm$`.
+contributors:
+ - fabian-hiller
+---
+
+import { Link } from '~/components';
+
+# Architecture
+
+You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support Qwik, SolidJS, React, Vue, Svelte and Preact without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.
+
+## Three packages, three responsibilities
+
+Formisch is structured as three packages, each with a clear role:
+
+- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
+- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and so on. Each method lives in its own file and only imports the core helpers it actually needs.
+- [`@formisch/qwik`](https://github.com/open-circle/formisch/tree/main/frameworks/qwik) is the thin user-facing layer. It provides the `useForm$`, `useField` and `useFieldArray` hooks, the ``, `` and `` components, and re-exports every method from `@formisch/methods` so you only import from a single package.
+
+```
+@formisch/qwik
+ ├─ useForm$, , , … (Qwik-specific reactive layer)
+ └─ re-exports @formisch/methods/qwik (focus, reset, validate, …)
+ └─ @formisch/core/qwik (createFormStore, types, signals)
+ └─ framework adapter (createSignal, batch, untrack, createId)
+```
+
+The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `useForm$`, `` and `` ships nothing else, even though `@formisch/qwik` re-exports every method.
+
+## One core, one adapter per framework
+
+The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/qwik`, `@formisch/core/solid`, `@formisch/core/react`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.
+
+The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.qwik.ts` (or `.solid.ts`, `.react.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.
+
+Qwik already has a native signal primitive in `@qwik.dev/core`, so [the Qwik adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.qwik.ts) is essentially a direct re-export:
+
+```ts
+export { createSignal, untrack } from '@qwik.dev/core';
+```
+
+The signals Qwik returns already match the `.value` getter/setter shape Formisch's core relies on. For frameworks that don't have a native signal primitive (like React), the adapter implements a small pub/sub system from scratch that exposes the same `Signal` shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.
+
+This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in real Qwik signals, fully resumable and serializable across the server/client boundary.
+
+That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Svelte user is a bug fixed for Qwik, Solid, React and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.
+
+## State lives in signals
+
+Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:
+
+```ts
+interface Signal {
+ get value(): T;
+ set value(nextValue: T): void;
+}
+```
+
+Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal — typically a single `` — re-render.
+
+This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.
+
+## What happens when you call `useForm$`
+
+When you call `useForm$`, three layers cooperate to produce the form store you receive.
+
+```tsx
+import { useForm$ } from '@formisch/qwik';
+import { component$ } from '@qwik.dev/core';
+import * as v from 'valibot';
+
+const LoginSchema = v.object({
+ email: v.pipe(v.string(), v.email()),
+ password: v.pipe(v.string(), v.minLength(8)),
+});
+
+export default component$(() => {
+ const loginForm = useForm$(() => ({ schema: LoginSchema }));
+ // …
+});
+```
+
+First, the [Qwik wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/qwik/src/hooks/useForm$/useForm$.ts) resolves the QRL configuration and memoizes the whole form construction inside `useConstant`, so the store is built exactly once and resumed across the server/client boundary instead of being recreated. Inside that callback, [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) is called with the configuration and a parse closure that captures your schema, then the public form store is assembled on top of the internal store it returns:
+
+```ts
+const form = useConstant(() => {
+ const internalFormStore = createFormStore(
+ {
+ ...config,
+ schema: JSON.parse(JSON.stringify(config.schema)),
+ },
+ $(async (input) =>
+ v.safeParseAsync((await configQrl.resolve())().schema, input)
+ )
+ );
+ return {
+ [INTERNAL]: internalFormStore,
+ isSubmitting: internalFormStore.isSubmitting,
+ isSubmitted: internalFormStore.isSubmitted,
+ isValidating: internalFormStore.isValidating,
+ isTouched: createComputed$(() =>
+ getFieldBool(internalFormStore, 'isTouched')
+ ),
+ isDirty: createComputed$(() => getFieldBool(internalFormStore, 'isDirty')),
+ isValid: createComputed$(() => !getFieldBool(internalFormStore, 'errors')),
+ errors: internalFormStore.errors,
+ };
+});
+```
+
+There's a Qwik-specific step worth calling out: the schema is round-tripped through `JSON.parse(JSON.stringify(...))` before it's stored on the internal store. The internal store needs to survive serialization for resumability, and stripping the schema to its plain-data shape ensures everything `createFormStore` puts on it can be sent to the client. Validation still uses the original schema, looked up through the QRL when needed.
+
+The interesting work happens inside `createFormStore`. It calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:
+
+- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
+- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
+- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
+- Wrapper schemas such as `v.optional`, `v.nullable` and `v.nonNullable` are unwrapped and recursed into. (`v.lazy` is not supported in Qwik builds and throws at form creation.)
+- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.
+
+At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.
+
+The form object the wrapper returns sits on top of that internal store. Form-level signals are exposed directly, while derived state like `isTouched`, `isDirty` and `isValid` is wrapped in `createComputed$` so it recomputes lazily as the underlying field signals change.
+
+The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.
+
+## Why this design pays off
+
+The same four properties keep coming back, and now you can see where they come from:
+
+- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/qwik`](https://github.com/open-circle/formisch/tree/main/frameworks/qwik), a form that uses only `useForm$` and `` never pulls in `validate`, `reset`, `insert` or the other operations. Tree-shaking gets to do real work.
+- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly and Qwik's signals handle propagation. Updating one field does not invalidate the rest.
+- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
+- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.
+
+## Further reading
+
+For a closer look at how Valibot's inference flows into your editor, see TypeScript. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
diff --git a/website/src/routes/(docs)/qwik/guides/menu.md b/website/src/routes/(docs)/qwik/guides/menu.md
index 9c194733..7e593560 100644
--- a/website/src/routes/(docs)/qwik/guides/menu.md
+++ b/website/src/routes/(docs)/qwik/guides/menu.md
@@ -22,3 +22,4 @@
- [Nested fields](/qwik/guides/nested-fields/)
- [Field arrays](/qwik/guides/field-arrays/)
- [TypeScript](/qwik/guides/typescript/)
+- [Architecture](/qwik/guides/architecture/)
diff --git a/website/src/routes/(docs)/react/guides/(advanced-guides)/architecture/index.mdx b/website/src/routes/(docs)/react/guides/(advanced-guides)/architecture/index.mdx
new file mode 100644
index 00000000..a82962b2
--- /dev/null
+++ b/website/src/routes/(docs)/react/guides/(advanced-guides)/architecture/index.mdx
@@ -0,0 +1,158 @@
+---
+title: Architecture
+description: >-
+ A look behind the scenes at how Formisch is structured. Learn how the core,
+ methods and framework packages fit together, how a single signal interface
+ plugs into each framework's native reactivity, and what happens when you
+ call `useForm`.
+contributors:
+ - fabian-hiller
+---
+
+import { Link } from '~/components';
+
+# Architecture
+
+You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support React, SolidJS, Vue, Svelte, Preact and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.
+
+## Three packages, three responsibilities
+
+Formisch is structured as three packages, each with a clear role:
+
+- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
+- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and so on. Each method lives in its own file and only imports the core helpers it actually needs.
+- [`@formisch/react`](https://github.com/open-circle/formisch/tree/main/frameworks/react) is the thin user-facing layer. It provides the `useForm`, `useField` and `useFieldArray` hooks, the ``, `` and `` components, and re-exports every method from `@formisch/methods` so you only import from a single package.
+
+```
+@formisch/react
+ ├─ useForm, , , … (React-specific reactive layer)
+ └─ re-exports @formisch/methods/react (focus, reset, validate, …)
+ └─ @formisch/core/react (createFormStore, types, signals)
+ └─ framework adapter (createSignal, batch, untrack, createId)
+```
+
+The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `useForm`, `` and `` ships nothing else, even though `@formisch/react` re-exports every method.
+
+## One core, one adapter per framework
+
+The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/react`, `@formisch/core/solid`, `@formisch/core/vue`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.
+
+The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.react.ts` (or `.solid.ts`, `.vue.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.
+
+Since React doesn't have a native signal primitive, [the React adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.react.ts) implements a small pub/sub system from scratch that exposes the `Signal` shape:
+
+```ts
+export function createSignal(value: T): Signal {
+ const subscribers = new Set();
+ return {
+ get value() {
+ if (listener) {
+ subscribers.add(listener);
+ listener[1].add(subscribers);
+ }
+ return value;
+ },
+ set value(newValue: T) {
+ if (newValue !== value) {
+ value = newValue;
+ // notify subscribers …
+ }
+ },
+ };
+}
+```
+
+A companion [`useSignals`](https://github.com/open-circle/formisch/blob/main/frameworks/react/src/hooks/useSignals/useSignals.ts) hook in the React wrapper registers your component as a listener and forces a re-render when any signal it read changes. For frameworks that already have native signals (Solid, Preact, Vue, Svelte, Qwik), the adapter is a thin wrapper around the framework's own primitive — sometimes just a single re-export. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.
+
+This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. In React, form state lives in the same minimal pub/sub the wrapper already needs to subscribe components, with no second abstraction on top.
+
+That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Svelte user is a bug fixed for React, Solid, Vue and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.
+
+## State lives in signals
+
+Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:
+
+```ts
+interface Signal {
+ get value(): T;
+ set value(nextValue: T): void;
+}
+```
+
+Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal — typically a single `` — re-render.
+
+This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.
+
+## What happens when you call `useForm`
+
+When you call `useForm`, three layers cooperate to produce the form store you receive.
+
+```tsx
+import { useForm } from '@formisch/react';
+import * as v from 'valibot';
+
+const LoginSchema = v.object({
+ email: v.pipe(v.string(), v.email()),
+ password: v.pipe(v.string(), v.minLength(8)),
+});
+
+function LoginForm() {
+ const loginForm = useForm({ schema: LoginSchema });
+ // …
+}
+```
+
+First, the [React wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/react/src/hooks/useForm/useForm.ts) hands the configuration to the core function [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) inside a `useMemo`, together with a parse closure that captures your schema:
+
+```ts
+const internalFormStore = useMemo(
+ () =>
+ createFormStore(config, (input) => v.safeParseAsync(config.schema, input)),
+ []
+);
+```
+
+Next, `createFormStore` calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:
+
+- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
+- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
+- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
+- Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into.
+- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.
+
+At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.
+
+Finally, the React wrapper builds the public form store you actually receive. It's a small object that sits on top of the internal store: form-level signals are exposed through getters that read `internalFormStore.isSubmitting.value` and friends, while derived state like `isTouched`, `isDirty` and `isValid` is computed on demand from the field tree. The whole object is memoized so it survives re-renders:
+
+```ts
+return useMemo(
+ () => ({
+ [INTERNAL]: internalFormStore,
+ get isSubmitting() {
+ return internalFormStore.isSubmitting.value;
+ },
+ get isTouched() {
+ return getFieldBool(internalFormStore, 'isTouched');
+ },
+ // …
+ }),
+ [internalFormStore]
+);
+```
+
+At the very top of the hook body, before any of this, the wrapper calls `useSignals()` to bridge the signals into React's render cycle: it registers the component as a listener so any signal read by the getters above will trigger a re-render when it changes.
+
+The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.
+
+## Why this design pays off
+
+The same four properties keep coming back, and now you can see where they come from:
+
+- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/react`](https://github.com/open-circle/formisch/tree/main/frameworks/react), a form that uses only `useForm` and `` never pulls in `validate`, `reset`, `insert` or the other operations. Tree-shaking gets to do real work.
+- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly, and only the components subscribed to that specific signal re-render. Updating one field does not invalidate the rest.
+- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
+- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.
+
+## Further reading
+
+To see how these design choices stack up against Felte and TanStack Form, head to the comparison guide. For a closer look at how Valibot's inference flows into your editor, see TypeScript. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
diff --git a/website/src/routes/(docs)/react/guides/menu.md b/website/src/routes/(docs)/react/guides/menu.md
index e3815c18..f8e10d9c 100644
--- a/website/src/routes/(docs)/react/guides/menu.md
+++ b/website/src/routes/(docs)/react/guides/menu.md
@@ -23,3 +23,4 @@
- [Nested fields](/react/guides/nested-fields/)
- [Field arrays](/react/guides/field-arrays/)
- [TypeScript](/react/guides/typescript/)
+- [Architecture](/react/guides/architecture/)
diff --git a/website/src/routes/(docs)/solid/guides/(advanced-guides)/architecture/index.mdx b/website/src/routes/(docs)/solid/guides/(advanced-guides)/architecture/index.mdx
new file mode 100644
index 00000000..29688cce
--- /dev/null
+++ b/website/src/routes/(docs)/solid/guides/(advanced-guides)/architecture/index.mdx
@@ -0,0 +1,147 @@
+---
+title: Architecture
+description: >-
+ A look behind the scenes at how Formisch is structured. Learn how the core,
+ methods and framework packages fit together, how a single signal interface
+ plugs into each framework's native reactivity, and what happens when you
+ call `createForm`.
+contributors:
+ - fabian-hiller
+---
+
+import { Link } from '~/components';
+
+# Architecture
+
+You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support SolidJS, React, Vue, Svelte, Preact and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.
+
+## Three packages, three responsibilities
+
+Formisch is structured as three packages, each with a clear role:
+
+- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
+- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and so on. Each method lives in its own file and only imports the core helpers it actually needs.
+- [`@formisch/solid`](https://github.com/open-circle/formisch/tree/main/frameworks/solid) is the thin user-facing layer. It provides the `createForm`, `useField` and `useFieldArray` primitives, the ``, `` and `` components, and re-exports every method from `@formisch/methods` so you only import from a single package.
+
+```
+@formisch/solid
+ ├─ createForm, , , … (Solid-specific reactive layer)
+ └─ re-exports @formisch/methods/solid (focus, reset, validate, …)
+ └─ @formisch/core/solid (createFormStore, types, signals)
+ └─ framework adapter (createSignal, batch, untrack, createId)
+```
+
+The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `createForm`, `` and `` ships nothing else, even though `@formisch/solid` re-exports every method.
+
+## One core, one adapter per framework
+
+The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/solid`, `@formisch/core/react`, `@formisch/core/svelte`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.
+
+The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.solid.ts` (or `.react.ts`, `.svelte.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.
+
+For Solid, [the adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.solid.ts) is a thin wrapper around the framework's own signal primitive:
+
+```ts
+import { createSignal as signal } from 'solid-js';
+
+export function createSignal(initialValue: T): Signal {
+ const [getSignal, setSignal] = signal(initialValue);
+ return {
+ get value() {
+ return getSignal();
+ },
+ set value(nextValue: T) {
+ setSignal(() => nextValue);
+ },
+ };
+}
+```
+
+For frameworks that don't have native signals — like React — [the adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.react.ts) implements a small pub/sub system that exposes the same `Signal` shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.
+
+This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in Solid's own signals, with full access to Solid's batching, scheduling and fine-grained tracking.
+
+That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Svelte user is a bug fixed for Solid, React, Vue and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.
+
+## State lives in signals
+
+Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:
+
+```ts
+interface Signal {
+ get value(): T;
+ set value(nextValue: T): void;
+}
+```
+
+Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the consumers of that one signal — typically a single `` — re-render.
+
+This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.
+
+## What happens when you call `createForm`
+
+When you call `createForm`, three layers cooperate to produce the form store you receive.
+
+```tsx
+import { createForm } from '@formisch/solid';
+import * as v from 'valibot';
+
+const LoginSchema = v.object({
+ email: v.pipe(v.string(), v.email()),
+ password: v.pipe(v.string(), v.minLength(8)),
+});
+
+const loginForm = createForm({ schema: LoginSchema });
+```
+
+First, the [Solid wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/solid/src/primitives/createForm/createForm.ts) hands the configuration to the core function [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts), together with a parse closure that captures your schema:
+
+```ts
+const internalFormStore = createFormStore(config, (input) =>
+ v.safeParseAsync(config.schema, input)
+);
+```
+
+Next, `createFormStore` calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:
+
+- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
+- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
+- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
+- Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into.
+- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.
+
+At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.
+
+Finally, the Solid wrapper builds the public form store you actually receive. It's a small reactive object that sits on top of the internal store: form-level signals are exposed through getters that read `internalFormStore.isSubmitting.value` and friends, while derived state like `isTouched`, `isDirty` and `isValid` is wrapped in `createMemo` so it recomputes lazily as the underlying field signals change:
+
+```ts
+const getIsTouched = createMemo(() =>
+ getFieldBool(internalFormStore, 'isTouched')
+);
+
+const form = {
+ [INTERNAL]: internalFormStore,
+ get isSubmitting() {
+ return internalFormStore.isSubmitting.value;
+ },
+ get isTouched() {
+ return getIsTouched();
+ },
+ // …
+};
+```
+
+The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.
+
+## Why this design pays off
+
+The same four properties keep coming back, and now you can see where they come from:
+
+- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/solid`](https://github.com/open-circle/formisch/tree/main/frameworks/solid), a form that uses only `createForm` and `` never pulls in `validate`, `reset`, `insert` or the other operations. Tree-shaking gets to do real work.
+- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods mutate `.value` directly and Solid's own reactivity handles propagation. Updating one field does not invalidate the rest.
+- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
+- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.
+
+## Further reading
+
+To see how these design choices stack up against Felte and TanStack Form, head to the comparison guide. For a closer look at how Valibot's inference flows into your editor, see TypeScript. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
diff --git a/website/src/routes/(docs)/solid/guides/menu.md b/website/src/routes/(docs)/solid/guides/menu.md
index 683b1f57..cd5576c7 100644
--- a/website/src/routes/(docs)/solid/guides/menu.md
+++ b/website/src/routes/(docs)/solid/guides/menu.md
@@ -23,3 +23,4 @@
- [Nested fields](/solid/guides/nested-fields/)
- [Field arrays](/solid/guides/field-arrays/)
- [TypeScript](/solid/guides/typescript/)
+- [Architecture](/solid/guides/architecture/)
diff --git a/website/src/routes/(docs)/svelte/guides/(advanced-guides)/architecture/index.mdx b/website/src/routes/(docs)/svelte/guides/(advanced-guides)/architecture/index.mdx
new file mode 100644
index 00000000..e52aa19b
--- /dev/null
+++ b/website/src/routes/(docs)/svelte/guides/(advanced-guides)/architecture/index.mdx
@@ -0,0 +1,147 @@
+---
+title: Architecture
+description: >-
+ A look behind the scenes at how Formisch is structured. Learn how the core,
+ methods and framework packages fit together, how a single signal interface
+ plugs into each framework's native reactivity, and what happens when you
+ call `createForm`.
+contributors:
+ - fabian-hiller
+---
+
+import { Link } from '~/components';
+
+# Architecture
+
+You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support Svelte, SolidJS, React, Vue, Preact and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.
+
+## Three packages, three responsibilities
+
+Formisch is structured as three packages, each with a clear role:
+
+- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
+- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and so on. Each method lives in its own file and only imports the core helpers it actually needs.
+- [`@formisch/svelte`](https://github.com/open-circle/formisch/tree/main/frameworks/svelte) is the thin user-facing layer. It provides the `createForm`, `useField` and `useFieldArray` runes, the ``, `` and `` components, and re-exports every method from `@formisch/methods` so you only import from a single package.
+
+```
+@formisch/svelte
+ ├─ createForm, , , … (Svelte-specific reactive layer)
+ └─ re-exports @formisch/methods/svelte (focus, reset, validate, …)
+ └─ @formisch/core/svelte (createFormStore, types, signals)
+ └─ framework adapter (createSignal, batch, untrack, createId)
+```
+
+The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `createForm`, `` and `` ships nothing else, even though `@formisch/svelte` re-exports every method.
+
+## One core, one adapter per framework
+
+The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/svelte`, `@formisch/core/solid`, `@formisch/core/react`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.
+
+The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.svelte.ts` (or `.solid.ts`, `.react.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.
+
+For Svelte, [the adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.svelte.ts) is a thin wrapper around `$state.raw` that exposes the `Signal` shape Formisch's core relies on:
+
+```ts
+export function createSignal(initialValue: T): Signal {
+ let signal = $state.raw(initialValue);
+ return {
+ get value() {
+ return signal;
+ },
+ set value(nextValue: T) {
+ signal = nextValue;
+ },
+ };
+}
+```
+
+For frameworks that don't have a native signal primitive (like React), the adapter implements a small pub/sub system from scratch that exposes the same shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.
+
+This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in real `$state.raw` cells, integrated with Svelte's compiler-driven fine-grained tracking.
+
+That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Solid user is a bug fixed for Svelte, React, Vue and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.
+
+## State lives in signals
+
+Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:
+
+```ts
+interface Signal {
+ get value(): T;
+ set value(nextValue: T): void;
+}
+```
+
+Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal — typically a single `` — re-render.
+
+This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.
+
+## What happens when you call `createForm`
+
+When you call `createForm`, three layers cooperate to produce the form store you receive.
+
+```svelte
+
+```
+
+First, the [Svelte wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/svelte/src/runes/createForm/createForm.svelte.ts) hands the configuration to the core function [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts), together with a parse closure that captures your schema:
+
+```ts
+const internalFormStore = createFormStore(config, (input) =>
+ v.safeParseAsync(config.schema, input)
+);
+```
+
+Next, `createFormStore` calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:
+
+- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
+- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
+- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
+- Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into.
+- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.
+
+At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.
+
+Finally, the Svelte wrapper builds the public form store you actually receive. It's a small reactive object that sits on top of the internal store: form-level signals are exposed through getters that read `internalFormStore.isSubmitting.value` and friends, while derived state like `isTouched`, `isDirty` and `isValid` is declared with `$derived` so it recomputes lazily as the underlying field signals change:
+
+```ts
+const isTouched = $derived(getFieldBool(internalFormStore, 'isTouched'));
+const isDirty = $derived(getFieldBool(internalFormStore, 'isDirty'));
+const isValid = $derived(!getFieldBool(internalFormStore, 'errors'));
+
+return {
+ [INTERNAL]: internalFormStore,
+ get isSubmitting() {
+ return internalFormStore.isSubmitting.value;
+ },
+ get isTouched() {
+ return isTouched;
+ },
+ // …
+};
+```
+
+The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.
+
+## Why this design pays off
+
+The same four properties keep coming back, and now you can see where they come from:
+
+- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/svelte`](https://github.com/open-circle/formisch/tree/main/frameworks/svelte), a form that uses only `createForm` and `` never pulls in `validate`, `reset`, `insert` or the other operations. Tree-shaking gets to do real work.
+- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly and Svelte's compiler-driven tracking handles propagation. Updating one field does not invalidate the rest.
+- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
+- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.
+
+## Further reading
+
+To see how these design choices stack up against alternatives, head to the comparison guide. For a closer look at how Valibot's inference flows into your editor, see TypeScript. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
diff --git a/website/src/routes/(docs)/svelte/guides/menu.md b/website/src/routes/(docs)/svelte/guides/menu.md
index 9c41c5df..725b47b4 100644
--- a/website/src/routes/(docs)/svelte/guides/menu.md
+++ b/website/src/routes/(docs)/svelte/guides/menu.md
@@ -23,3 +23,4 @@
- [Nested fields](/svelte/guides/nested-fields/)
- [Field arrays](/svelte/guides/field-arrays/)
- [TypeScript](/svelte/guides/typescript/)
+- [Architecture](/svelte/guides/architecture/)
diff --git a/website/src/routes/(docs)/vue/guides/(advanced-guides)/architecture/index.mdx b/website/src/routes/(docs)/vue/guides/(advanced-guides)/architecture/index.mdx
new file mode 100644
index 00000000..ed93f14d
--- /dev/null
+++ b/website/src/routes/(docs)/vue/guides/(advanced-guides)/architecture/index.mdx
@@ -0,0 +1,139 @@
+---
+title: Architecture
+description: >-
+ A look behind the scenes at how Formisch is structured. Learn how the core,
+ methods and framework packages fit together, how a single signal interface
+ plugs into each framework's native reactivity, and what happens when you
+ call `useForm`.
+contributors:
+ - fabian-hiller
+---
+
+import { Link } from '~/components';
+
+# Architecture
+
+You don't need to read this guide to use Formisch. It's here for the curious — for people who want to understand why the bundle stays small, why updates are fine-grained, and how the same library can support Vue, SolidJS, React, Svelte, Preact and Qwik without forking the codebase. Each of those properties falls out of a few deliberate architectural choices.
+
+## Three packages, three responsibilities
+
+Formisch is structured as three packages, each with a clear role:
+
+- [`@formisch/core`](https://github.com/open-circle/formisch/tree/main/packages/core) is the framework-agnostic foundation. It contains the form and field store types, the recursive store builder that mirrors your Valibot schema, and the validation orchestration.
+- [`@formisch/methods`](https://github.com/open-circle/formisch/tree/main/packages/methods) exposes the form operations as standalone, tree-shakeable functions: `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and so on. Each method lives in its own file and only imports the core helpers it actually needs.
+- [`@formisch/vue`](https://github.com/open-circle/formisch/tree/main/frameworks/vue) is the thin user-facing layer. It provides the `useForm`, `useField` and `useFieldArray` composables, the ``, `` and `` components, and re-exports every method from `@formisch/methods` so you only import from a single package.
+
+```
+@formisch/vue
+ ├─ useForm, , , … (Vue-specific reactive layer)
+ └─ re-exports @formisch/methods/vue (focus, reset, validate, …)
+ └─ @formisch/core/vue (createFormStore, types, signals)
+ └─ framework adapter (createSignal, batch, untrack, createId)
+```
+
+The modular architecture is what makes Formisch tree-shakeable. The core stays small, and every additional capability — `setInput`, `validate`, `reset`, `insert`, `move`, `focus` and the rest — is exported as its own function. A form that only imports `useForm`, `` and `` ships nothing else, even though `@formisch/vue` re-exports every method.
+
+## One core, one adapter per framework
+
+The core package is framework-agnostic, but it still needs _some_ reactivity primitive to build on. Formisch handles this by providing a subpath export for every supported framework (`@formisch/core/vue`, `@formisch/core/solid`, `@formisch/core/react`, …), all built from the same source. The only file that differs between them [is a small adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.ts) that exports four functions: `createSignal`, `batch`, `untrack` and `createId`.
+
+The swap happens at build time, not at runtime. A small [rolldown plugin](https://github.com/open-circle/formisch/blob/main/packages/core/tsdown.config.ts) sits in front of import resolution: whenever the bundler resolves an import of `./framework/index.ts`, the plugin looks for a sibling `./framework/index.vue.ts` (or `.solid.ts`, `.react.ts`, …) and, if it exists, redirects the import there. The build runs once per framework, and each run produces a self-contained bundle with the framework-specific adapter inlined as if it had always been the only option.
+
+Vue's own `shallowRef` already implements the `.value` getter/setter shape Formisch's core relies on, so [the Vue adapter](https://github.com/open-circle/formisch/blob/main/packages/core/src/framework/index.vue.ts) is essentially a direct re-export:
+
+```ts
+export { shallowRef as createSignal } from 'vue';
+```
+
+For frameworks that don't have a native signal primitive (like React), the adapter implements a small pub/sub system from scratch that exposes the same `Signal` shape. Every other file in `@formisch/core` only ever imports `createSignal`, `batch`, `untrack` and `createId`, never a framework directly. That's why the same [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts) code path can power every framework binding without a single conditional.
+
+This is what makes Formisch different from most framework-agnostic libraries. The usual approach is to ship a runtime abstraction — a custom store, an observer protocol, a subscription layer — that lives in the bundle and has to be bridged into each framework. Formisch ships nothing like that. Where the framework has native signals, Formisch uses them directly; where it doesn't, the adapter is just enough pub/sub to drive re-renders. Either way, there is no extra layer between your form state and the framework's reactivity, and your bundle pays no overhead for portability. Form state lives in real Vue `shallowRef` instances, fully integrated with Vue's reactivity system and dependency tracking.
+
+That Formisch is framework-agnostic is not a trade-off you make — it's a benefit you get. A bug surfaced by a Svelte user is a bug fixed for Vue, Solid, React and everyone else. An improvement to the recursive store builder, the validation orchestration or any individual method lands across every binding at once. There is one library to maintain, not one per framework, and every community pulls the rest forward.
+
+## State lives in signals
+
+Every piece of reactive state in Formisch is a [`Signal`](https://github.com/open-circle/formisch/blob/main/packages/core/src/types/signal/signal.ts) with the same minimal interface:
+
+```ts
+interface Signal {
+ get value(): T;
+ set value(nextValue: T): void;
+}
+```
+
+Reads track, writes notify. There is no central store object that gets diffed and no equality check on the consumer side. Each field carries its own signals for `errors`, `isTouched`, `isDirty`, plus an `input` signal for value fields and a `children` collection for arrays and objects. When you call `setInput(form, { path: ['email'], input: 'a@b.c' })`, only the `input` signal of that one field is written, so only the components that read that one signal — typically a single `` — re-render.
+
+This is the source of the fine-grained reactivity. The shape of the store is pre-allocated at form creation, matching the shape of your Valibot schema, so methods never have to look up paths in a generic object — they walk a typed tree of stores and update individual signals.
+
+## What happens when you call `useForm`
+
+When you call `useForm`, three layers cooperate to produce the form store you receive.
+
+```vue
+
+```
+
+First, the [Vue wrapper](https://github.com/open-circle/formisch/blob/main/frameworks/vue/src/composables/useForm/useForm.ts) hands the configuration to the core function [`createFormStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/form/createFormStore/createFormStore.ts), together with a parse closure that captures your schema:
+
+```ts
+const internalFormStore = createFormStore(config, (input) =>
+ v.safeParseAsync(config.schema, input)
+);
+```
+
+Next, `createFormStore` calls [`initializeFieldStore`](https://github.com/open-circle/formisch/blob/main/packages/core/src/field/initializeFieldStore/initializeFieldStore.ts), which walks your Valibot schema recursively and builds the internal store hierarchy:
+
+- A `v.object(...)` becomes an `InternalObjectStore` with a `children` record keyed by field name.
+- A `v.array(...)` or `v.tuple(...)` becomes an `InternalArrayStore` with a `children` list and its own `initialInput`, `startInput` and `input` signals so it can track structural changes.
+- A leaf like `v.string()` or `v.number()` becomes an `InternalValueStore` with an `input` signal that holds the current field value.
+- Wrapper schemas such as `v.optional`, `v.nullable`, `v.nonNullable` and `v.lazy` are unwrapped and recursed into.
+- `v.union`, `v.variant` and `v.intersect` initialize every option into the same store, which is what lets you address discriminated fields with a single field path.
+
+At every level, the same three signals are created: `errors`, `isTouched` and `isDirty`. Once the field tree is built, three more signals are added to the form root: `isSubmitting`, `isSubmitted` and `isValidating`.
+
+Finally, the Vue wrapper builds the public form store you actually receive. It's a small reactive object that sits on top of the internal store: form-level signals are exposed through getters that read `internalFormStore.isSubmitting.value` and friends, while derived state like `isTouched`, `isDirty` and `isValid` is wrapped in `computed` so it recomputes lazily as the underlying field signals change:
+
+```ts
+const isTouched = computed(() =>
+ getFieldBool(internalFormStore, 'isTouched')
+);
+const isDirty = computed(() => getFieldBool(internalFormStore, 'isDirty'));
+const isValid = computed(() => !getFieldBool(internalFormStore, 'errors'));
+
+return {
+ [INTERNAL]: internalFormStore,
+ get isSubmitting() {
+ return internalFormStore.isSubmitting.value;
+ },
+ get isTouched() {
+ return isTouched.value;
+ },
+ // …
+};
+```
+
+The internal store is tucked away behind the [`INTERNAL`](https://github.com/open-circle/formisch/blob/main/packages/core/src/values.ts) symbol. Methods like `setInput(form, …)` reach in via `form[INTERNAL]` to mutate signals, while your code only sees the public form store. This separation is what keeps the public API small and stable even as the internal shape evolves.
+
+## Why this design pays off
+
+The same four properties keep coming back, and now you can see where they come from:
+
+- **Small bundle size.** Because methods are individual modules re-exported from [`@formisch/vue`](https://github.com/open-circle/formisch/tree/main/frameworks/vue), a form that uses only `useForm` and `` never pulls in `validate`, `reset`, `insert` or the other operations. Tree-shaking gets to do real work.
+- **Fine-grained reactivity.** The store shape is pre-allocated at form creation, with one signal per piece of state. Methods write `.value` directly and Vue's reactivity system handles propagation. Updating one field does not invalidate the rest.
+- **Framework portability.** The entire library is parameterized over a four-function adapter. Adding a new framework means writing a small adapter that maps `createSignal`, `batch`, `untrack` and `createId` onto its primitives.
+- **Type safety.** Your Valibot schema is the single source of truth. It drives both runtime parsing and the inferred TypeScript types for paths, inputs and outputs, so there is no second declaration to keep in sync.
+
+## Further reading
+
+To see how these design choices stack up against alternatives, head to the comparison guide. For a closer look at how Valibot's inference flows into your editor, see TypeScript. And if you want to read the recursive walker, the adapter implementations, or how individual methods are written, the full source is on [GitHub](https://github.com/open-circle/formisch).
diff --git a/website/src/routes/(docs)/vue/guides/menu.md b/website/src/routes/(docs)/vue/guides/menu.md
index d6df4ea3..8a277ee3 100644
--- a/website/src/routes/(docs)/vue/guides/menu.md
+++ b/website/src/routes/(docs)/vue/guides/menu.md
@@ -23,3 +23,4 @@
- [Nested fields](/vue/guides/nested-fields/)
- [Field arrays](/vue/guides/field-arrays/)
- [TypeScript](/vue/guides/typescript/)
+- [Architecture](/vue/guides/architecture/)