Skip to content

Navigation Plugin

olegivanov edited this page Jun 2, 2026 · 7 revisions

@real-router/navigation-plugin

1. Overview

  • Name: Navigation Plugin
  • Package: @real-router/navigation-plugin
  • Purpose: Integrates Real Router with the browser Navigation API, synchronizing router state with browser URL and providing route-level history access unavailable in the History API.
  • Typical scenarios: SPAs targeting modern browsers where you need to inspect session history entries, check which routes a user has visited, or jump directly to a past history entry without stepping through intermediate pages.

2. Installation and Setup

npm install @real-router/navigation-plugin
# or
pnpm add @real-router/navigation-plugin
import { createRouter } from "@real-router/core";
import { navigationPluginFactory } from "@real-router/navigation-plugin";

const router = createRouter(routes);
router.usePlugin(navigationPluginFactory(options));

Peer dependency: @real-router/core

3. Configuration Options

Option Type Default Description
base string "" Base path for all routes (e.g., "/app")
forceDeactivate boolean false If true, browser back/forward skip canDeactivate guards. Default false respects guards — matches browser-plugin and the core router's guard contract.

Same two options as browser-plugin. The plugins are interchangeable at the options level.

Breaking change in plugin v0.6.0 (#524): the previous default was true, which silently bypassed every canDeactivate guard on browser back/forward. Apps that depended on that bypass must now opt in explicitly: navigationPluginFactory({ forceDeactivate: true }).

Looking for hash-based routing? Use @real-router/hash-plugin instead.

Configuration Examples

// Minimal configuration — canDeactivate guards run on browser back/forward by default
navigationPluginFactory();

// With base path
navigationPluginFactory({ base: "/app" });
// URL: example.com/app/users

// Opt in to bypassing canDeactivate guards on back/forward (old default)
navigationPluginFactory({ forceDeactivate: true });

Options Validation

The plugin performs runtime validation at factory time and throws a plain Error with a descriptive message on any violation.

Type checks

  • Validates option types against defaults (string for base, boolean for forceDeactivate).

base rules (via safeBaseRule from the shared browser-env)

  • Must not contain control characters (\u0000-\u001F, \u007F).
  • Must not contain .. path segments (e.g., /app/../evil).
  • Normalised to canonical form via normalizeBase: leading /, no trailing /, no // runs. "app""/app", "/app/""/app", "//app//""/app", "/""".
navigationPluginFactory({ base: "../evil" });   // throws: must not contain '..' segments
navigationPluginFactory({ base: "/app\nX" });   // throws: must not contain control characters

4. Why Navigation API?

The Navigation API (~89% global coverage as of 2026) gives you access to the full session history as structured data. Unlike the History API, you can inspect every entry, check what routes the user has visited, and traverse directly to a specific past entry.

// Not possible with browser-plugin:
router.peekBack(); // what's one step back?
router.hasVisited("checkout"); // did the user visit checkout?
router.getVisitedRoutes(); // all routes in this session
router.traverseToLast("users.list"); // jump back to the last users list

Browser support: Chrome 102+, Firefox 147+, Safari 26.2+, Opera 88+. For broader support, use @real-router/browser-plugin as a fallback (see Feature Detection).

Desktop compatibility

navigation-plugin runs in desktop WebViews too, but availability depends on the host:

  • Electron — always supported (Chromium ships the Navigation API since Chrome 102, 2022).
  • Tauri Windows / Android — supported (Chromium-based WebView).
  • Tauri macOS / iOS / Linux — depends on the WebKit version. See the full matrix in the Desktop Integration Guide.

On an unsupported WebView the factory throws immediately:

Error: [navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.

This is intentional (fail-fast) — the exclusive methods (peekBack, hasVisited, traverseToLast, etc.) cannot be emulated without the API.

5. Lifecycle Hooks

The plugin implements the following hooks:

Hook Implemented Description
onStart Subscribes to Navigation API navigate events
onStop Unsubscribes from navigate events
onTransitionStart Writes captured NavigationMeta to state.context.navigation (browser-initiated only — programmatic router.navigate() has no captured meta at this point, so guards see toState.context.navigation as undefined)
onTransitionSuccess Updates NavigationHistoryEntry state and browser URL
onTransitionError Clears capturedMeta and pendingTraverseKey
onTransitionCancel Clears capturedMeta and pendingTraverseKey
teardown Cleans up listeners, removes router extensions

Hook Implementation Details

onStart

  • Subscribes to the Navigation API navigate event
  • Removes existing listener if plugin is restarted

onTransitionSuccess

  • Determines: push (new entry) or replace (replacement) via shouldReplaceHistory
  • replace is used when:
    • First navigation (fromState is absent) and replace is not explicitly false
    • replace: true option
    • reload: true option with equal states
  • Preserves hash fragment when navigating to the same path (or on the first navigation with no fromState)
  • Note: passing replace: false on the first navigation forces push — this is an explicit user override

teardown

  • Removes event listeners
  • Removes start interceptor
  • Removes router extensions (buildUrl, matchUrl, replaceHistoryState, and all exclusive extensions) via extendRouter unsubscribe
  • Releases "navigation" context namespace claim

6. Router Extensions

Compatible Extensions (same as browser-plugin)

The plugin adds the following methods to the router instance via extendRouter():

Method Returns Description
buildUrl(name, params?, options?: { hash? }) string Build full URL with base path. Optional hash (decoded) is encoded and appended (#532).
matchUrl(url) State | undefined Parse URL to router state
replaceHistoryState(name, params?, options?: { hash? }) void Update browser URL without triggering navigation. Tri-state hash: undefined preserves, "" clears, value sets. (#532)
router.buildUrl("users", { id: "123" });
// => "/app/users/123" (with base "/app")

router.matchUrl("/app/users/123");
// => { name: "users", params: { id: "123" }, path: "/users/123" }

// Update URL silently (no transition, no guards)
router.replaceHistoryState("users", { id: "456" });

buildUrl vs buildPath

router.buildPath("users", { id: 1 }); // "/users/1"       — core, no base
router.buildUrl("users", { id: 1 }); // "/app/users/1"   — plugin, with base

replaceHistoryState vs navigate({ replace: true })

router.replaceHistoryState(name, params); // URL only, no transition
router.navigate(name, params, { replace: true }); // Full transition + URL update

Exclusive Extensions (Navigation API only)

These methods are only available with navigation-plugin. They have no equivalent in browser-plugin.

Method Returns Description
peekBack() State | undefined State of the previous history entry
peekForward() State | undefined State of the next history entry
hasVisited(routeName) boolean Whether any history entry matches the route
getVisitedRoutes() string[] Unique route names across all history entries
getRouteVisitCount(routeName) number How many history entries match the route
traverseToLast(routeName) Promise<State> Navigate to the last history entry for a route
canGoBack() boolean Whether there's a previous history entry
canGoForward() boolean Whether there's a next history entry
canGoBackTo(routeName) boolean Whether any previous entry matches the route

peekBack / peekForward

Preview where back/forward would take the user without navigating:

const prev = router.peekBack();
if (prev) {
  console.log(`Back goes to: ${prev.name}`);
}

const next = router.peekForward();
if (next) {
  console.log(`Forward goes to: ${next.name}`);
}

hasVisited / getVisitedRoutes / getRouteVisitCount

Inspect the session history to understand where the user has been:

// Check if the user has been to a route in this session
if (router.hasVisited("checkout")) {
  showResumeCheckoutBanner();
}

// Get all routes visited in this session
const visited = router.getVisitedRoutes();
// => ["home", "users.list", "users.view", "checkout"]

// How many times did the user visit the product page?
const count = router.getRouteVisitCount("products.view");

traverseToLast

Jump directly to the last time the user was on a given route. Skips intermediate entries without stepping through them one by one:

await router.traverseToLast("users.list");

canGoBack / canGoForward / canGoBackTo

Drive UI state for navigation controls:

// Disable back/forward buttons when there's nowhere to go
const backDisabled = !router.canGoBack();
const forwardDisabled = !router.canGoForward();

// Show "back to list" only if the user actually came from the list
if (router.canGoBackTo("users.list")) {
  showBackToListButton();
}

6a. State Context: state.context.url (#532)

In addition to state.context.navigation, navigation-plugin claims a second namespace state.context.url for URL fragment support. Both namespaces are populated on every successful transition.

interface UrlContext {
  /** Decoded URL fragment, no leading `#`. Empty string when URL has no fragment. */
  hash: string;
  /** True only on browser-driven hash-only navigation (Navigation API `event.hashChange === true`). */
  hashChanged: boolean;
}

Hash-only browser-driven clicks (event.hashChange === true from navigate-handler.ts) bypass core's SAME_STATES rejection via force: true, hashChange: true — subscribers fire normally on tab-style URLs.

Tri-state semantics for opts.hash in router.navigate(name, params, { hash }):

  • undefined (default) — preserve current hash
  • "" — clear the hash
  • "value" — set the hash

Same widening on router.buildUrl(name, params, { hash }) and router.replaceHistoryState(name, params, { hash }). Tri-state replaces the legacy shouldPreserveHash heuristic — there is no longer any "hash dropped on cross-path navigation" case.

Mutual Exclusivity

navigation-plugin and @real-router/browser-plugin both claim the "url" namespace — they cannot be installed on the same router. @real-router/hash-plugin is also mutually exclusive (it owns # as the route delimiter; see hash-plugin for the documented limitation).

For the full surface (encoding, F5 priming, hash-aware sources, recovery paths) see Hash.

6b. State Context: state.context.navigation

The navigation plugin writes navigation metadata to state.context.navigation on every transition. This replaces the former getNavigationMeta() router extension -- metadata is now directly on the state object, available in subscribe callbacks and components without calling a separate method.

NavigationMeta Type

interface NavigationMeta {
  /** Type of navigation: push, replace, traverse, or reload */
  navigationType: "push" | "replace" | "traverse" | "reload";
  /** Whether the navigation was initiated by the user (back/forward button, link click) */
  userInitiated: boolean;
  /** Ephemeral info passed via navigation.navigate({ info }) -- lost on page reload */
  info?: unknown;
  /** Direction of navigation in the history stack */
  direction: "forward" | "back" | "unknown";
  /** The DOM element that initiated the navigation (e.g., anchor tag), or null for programmatic */
  sourceElement: Element | null;
}

Module Augmentation

The plugin augments StateContext from @real-router/types:

declare module "@real-router/types" {
  interface StateContext {
    navigation?: NavigationMeta;
  }
}

Import the plugin package to activate type inference:

import "@real-router/navigation-plugin";

Usage Examples

// In subscribe callback
router.subscribe((state) => {
  const nav = state.context.navigation;
  if (nav) {
    console.log(nav.navigationType); // "push" | "replace" | "traverse" | "reload"
    console.log(nav.userInitiated);  // true if user clicked back/forward/link
    console.log(nav.direction);      // "forward" | "back" | "unknown"
    console.log(nav.info);           // data passed via navigation.navigate({ info })
  }
});
// In a React component
import { useRoute } from "@real-router/react";
import "@real-router/navigation-plugin";

function NavigationIndicator() {
  const { route } = useRoute();
  const nav = route?.context.navigation;

  if (nav?.direction === "back") {
    return <SlideRightTransition />;
  }
  return <SlideLeftTransition />;
}

Timing

The plugin writes context in two hooks:

  • onTransitionStart: Writes captured metadata from the Navigation API navigate event (browser-initiated navigations) or from the constructor's activation priming (cross-document loads — see below)
  • onTransitionSuccess: Writes or overwrites with the final metadata including derived navigationType and direction (all navigations)

Both writes are visible in subscribe() callbacks.

Cross-document load priming

After a cross-document navigation (F5, browser back/forward across the JS context boundary, fresh URL bar entry, external link click), the plugin reads navigation.activation.navigationType once in its constructor and primes the metadata for the first transition. This makes state.context.navigation.navigationType correctly report "reload" after F5 and "traverse" after cross-document back/forward — values that the in-document navigate event handler cannot observe (the JS context that handled the outgoing navigation no longer exists).

Browser support: Chrome 123+, Edge 123+, Firefox 147+, Safari 26.2+ (Baseline 2026). On Chrome 102–122 the first transition falls back to "replace".

Limitations on the primed first transition:

  • userInitiated is always false (the browser does not expose F5 vs. location.reload()).
  • direction is "forward" for "push", "unknown" otherwise (the activation does not disclose back vs. forward for "traverse").

7. Feature Detection

Use navigationPluginFactory when the Navigation API is available, fall back to browserPluginFactory otherwise:

import { browserPluginFactory } from "@real-router/browser-plugin";
import { navigationPluginFactory } from "@real-router/navigation-plugin";

const plugin =
  "navigation" in globalThis
    ? navigationPluginFactory({ base })
    : browserPluginFactory({ base });

router.usePlugin(plugin);

If "navigation" in globalThis is false and no custom browser is injected, the factory throws immediately:

Error: [navigation-plugin] Navigation API is not supported. Use @real-router/browser-plugin instead.

8. Navigation Flow

Router Navigation:
  navigate() → Promise<State> → onTransitionSuccess → navigation.navigate()

Browser Navigate Event:
  navigate event → handler → router.navigate() / navigateToNotFound() / $$error + intercept reject

The Navigation API serializes navigation via event.intercept() — only one navigation runs at a time. There's no deferred queue needed (unlike browser-plugin's popstate handling, which requires race condition protection).

Promise-Based API

All navigation methods return Promise<State>:

  • router.start(path?) — path is optional; the plugin injects browser.getLocation() when no path is given
  • router.navigate() — used in the navigate event handler with await and try/catch
  • router.navigateToNotFound(path?) — called when allowNotFound: true and the URL doesn't match any route
  • strict mode unmatched URL (allowNotFound: false) — emits $$error with ROUTE_NOT_FOUND via api.emitTransitionError(), then rejects event.intercept() so the Navigation API auto-rolls back the URL. No silent navigateToDefault() fallback.

9. Behavior

Main Scenarios

  • URL Building: buildUrl correctly adds base path
  • URL Matching: matchUrl parses URL via URL API, supports IPv6, Unicode
  • Browser History: push for new transitions, replace for replacement
  • Navigate Event: Handles back/forward buttons with full lifecycle transition

Edge Cases

  • Hash preservation (#532, tri-state default): Hash is preserved by default across any navigation — same path or different — when opts.hash is omitted (undefined). Pass opts.hash: "" to explicitly clear the fragment on a cross-path navigation, or opts.hash: "value" to set a new one. Section 6a documents the full tri-state contract. Prior to #532 (plugin v0.7.0) the heuristic dropped the hash on cross-path navigation; that behavior is gone.
  • SSR safety: In a non-browser environment, the plugin falls back to no-ops via createNavigationFallbackBrowser. No errors, methods return safe defaults.
  • CANNOT_DEACTIVATE / RouterError URL sync: When a guard rejects (or the router raises any RouterErrorCANNOT_DEACTIVATE, CANNOT_ACTIVATE, SAME_STATES, CANCELLED), the plugin calls syncUrlToRouterState internally — browser.navigate(url, { history: "replace" }) back to the router's current state. URL and router state stay in sync in a single visible transition. navigation.navigate().finished resolves (URL is valid, pointing back to the previous route); rejection-interested callers observe the error via the router's TRANSITION_ERROR / TRANSITION_CANCEL events. Manual sync is used instead of relying on Navigation API's native rollback on intercept rejection, which in Chromium headless and some cross-origin setups leaves a visible "committed-then-reverted" URL window that races with UI tests. Changed in plugin v0.6.0 (#524) — previously RouterError was silently swallowed and URL/state desynchronized.
  • Error recovery: recoverFromNavigateError handles non-RouterError exceptions (unexpected crashes) — logs [navigation-plugin] Critical error in navigate handler and calls the same syncUrlToRouterState helper.
  • Router-driven mutations re-enter the navigate handler (#518, #580): nav.navigate(...) and nav.traverseTo(...) fire navigate events. Chromium delivers them synchronously inside the call; Safari 26.2 WKWebView delivers them on a subsequent task. Detection is identity-based on event.info: createNavigationBrowser tags every router-driven mutation with info: PLUGIN_SYNC_INFO (a stable string constant exported from the package), and the navigate-event handler checks event.info === PLUGIN_SYNC_INFO to short-circuit with a noop intercept. The bare return is not enough — per Navigation API spec, a same-origin canIntercept event left un-intercepted triggers Chromium's cross-document fallback (full reload). The noop event.intercept({ handler, scroll: "manual" }) also overrides the Navigation API default scroll: "after-transition" so router-driven re-emits (scroll-spy hash-only nav, scroll-restoration URL sync) don't fight the app's own scroll motion — concrete bug closed: scroll-spy emit during a slow user scroll would otherwise trigger a viewport jump on every emit. Aligns with browser-plugin (History API has no auto-scroll on programmatic URL changes). Apps that want hash-anchor auto-scroll opt in via createScrollRestoration({ anchorScrolling: true }). Consumers supplying a custom NavigationBrowser must pass PLUGIN_SYNC_INFO as info in their own nav.navigate / nav.traverseTo calls.
  • Same-URL guard in onTransitionSuccess (#580): when the destination URL of a transition is canonically equal to the browser's current URL (initial transition into a route whose path already matches the bootstrap URL, router.navigate(name, params, { reload: true }) to current state, forwardTo redirects that don't change the path), the plugin writes router state via browser.updateCurrentEntry({ state }) instead of browser.navigate(url, { history: "replace" }). Both leave a single history entry, but updateCurrentEntry does not fire a navigate event — avoiding both the Chromium event round-trip and a WKWebView quirk under custom protocols (tauri://, app://) where same-URL nav.navigate({history:"replace"}) was treated as a cross-document navigation that discarded the JS context and triggered a render-loop on macOS 26.2 Tauri releases (#580). Behavioural consequence: same-URL transitions no longer fire navigate events. Consumers that branched on navigate events for state-only changes should use router.subscribe instead; state.context.navigation.navigationType still reflects the logical type (reload / replace). URL comparison uses the URL constructor, so scheme://host and scheme://host/ compare equal.
  • Missing state / unmatched URL: When allowNotFound: true — calls navigateToNotFound(browser.getLocation()) to preserve the URL. When allowNotFound: false — emits $$error with ROUTE_NOT_FOUND via api.emitTransitionError() and throws in the event.intercept() handler so the Navigation API auto-rolls back the URL. Router state is unchanged. defaultRoute is not consulted as an implicit fallback — see RouterOptions#allowNotFound for the userland migration snippet.
  • Defensive entry URL parsing: entryToState and the traverseToLast entry lookup delegate URL parsing to safeParseUrl (via extractPathFromAbsoluteUrl). Malformed entry URLs from mocks, extensions, or non-spec sources resolve to undefined instead of throwing from the navigate event handler — the Navigation API event proceeds through the "no matching route" branch.
  • nav.navigate options forwarding: browser.navigate(url, options) forwards the full options object to the Navigation API. Callers can pass info, downloadRequest, and any future Navigation API options transparently.

State in NavigationHistoryEntry

entry.getState() = {
  name: "users.view",
  params: { id: "123" },
  path: "/users/123",
};

Navigation metadata is available on state.context.navigation after each transition -- not stored in the history entry itself.

10. Browser Support

The plugin uses two API levels with different baselines:

Browser Navigation API (navigation) navigation.activation priming (#531)
Chrome 102+ 123+
Edge 102+ 123+
Opera 88+ 109+
Firefox 147+ 147+
Safari 26.2+ 26.2+
  • Navigation API baseline (col. 2)globalThis.navigation plus event.intercept are present. All core flows (URL sync, navigate event handling, history extensions) work. Below this, the factory throws ("Navigation API is not supported") and you should fall back to browser-plugin via Feature Detection.
  • Activation priming baseline (col. 3)navigation.activation.navigationType is exposed. On these browsers the first transition after a cross-document load (F5, back/forward across pages, fresh URL bar entry) reports the correct state.context.navigation.navigationType ("reload", "traverse", "push", "replace"). On the lower tier (Chrome 102–122, Opera 88–108), getActivationType() returns undefined and the first transition falls back to the legacy "replace" classification. Same-document navigation is unaffected. See §6b Cross-document load priming.

~89% global coverage as of 2026 for col. 2; the priming tier (col. 3) is Baseline 2026. For the remaining ~11%, use the Feature Detection pattern to fall back to browser-plugin.

11. Migration from browser-plugin

Switching from browser-plugin to navigation-plugin is a one-line import change. Options are identical.

// Before
import { browserPluginFactory } from "@real-router/browser-plugin";
router.usePlugin(browserPluginFactory({ base: "/app" }));

// After
import { navigationPluginFactory } from "@real-router/navigation-plugin";
router.usePlugin(navigationPluginFactory({ base: "/app" }));

All existing behavior is preserved. The exclusive extensions (peekBack, hasVisited, etc.) become available immediately after the swap.

12. Form Protection

Set forceDeactivate: false to respect canDeactivate guards on back/forward:

router.usePlugin(navigationPluginFactory({ forceDeactivate: false }));

import { getLifecycleApi } from "@real-router/core/api";

const lifecycle = getLifecycleApi(router);
lifecycle.addDeactivateGuard(
  "checkout",
  (router, getDep) => (toState, fromState) => {
    return !hasUnsavedChanges(); // false blocks back/forward
  },
);

13. Usage Examples

Basic Example

import { createRouter } from "@real-router/core";
import { navigationPluginFactory } from "@real-router/navigation-plugin";

const routes = [
  { name: "home", path: "/" },
  { name: "users", path: "/users" },
  { name: "users.view", path: "/view/:id" },
];

const router = createRouter(routes, {
  defaultRoute: "home",
});

router.usePlugin(navigationPluginFactory());
await router.start();

// Navigation
await router.navigate("users.view", { id: "123" });

// Building URL
const url = router.buildUrl("users.view", { id: "123" });
// => "/users/view/123"

// Matching URL
const state = router.matchUrl("https://example.com/users/view/456");
// => { name: "users.view", params: { id: "456" }, ... }

// History inspection
const prev = router.peekBack();
const visited = router.getVisitedRoutes();

Advanced Example with Feature Detection

import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";
import { navigationPluginFactory } from "@real-router/navigation-plugin";
import { loggerPluginFactory } from "@real-router/logger-plugin";
import { persistentParamsPluginFactory } from "@real-router/persistent-params-plugin";

const routes = [
  { name: "home", path: "/" },
  { name: "users", path: "/users?page&sort" },
];

const router = createRouter(routes, {
  defaultRoute: "home",
  queryParamsMode: "default",
});

const base = "/app";
const plugin =
  "navigation" in globalThis
    ? navigationPluginFactory({ base, forceDeactivate: false })
    : browserPluginFactory({ base, forceDeactivate: false });

// Plugin order matters!
router.usePlugin(loggerPluginFactory());
router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
router.usePlugin(plugin);

await router.start();

See Also

  • @real-router/browser-plugin — History API integration (broader browser support)
  • @real-router/hash-plugin — Hash-based routing (no server config needed)
  • Plugin Architecture — How plugins work in Real Router
  • Guards — Navigation guards and canDeactivate
  • Hash — URL fragment support via state.context.url (this plugin is one of two URL plugins that claim it)
  • Scroll Restorationstate.transition.replace skip path under this plugin (no magnetic snap on programmatic replaces)
  • Scroll Spyevent.intercept({ scroll: "manual" }) override eliminates viewport jumps during scroll-spy emits

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

SSR-feature subpath — @real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.

  • Lazy (Svelte only — dynamic component import)
  • Await — read deferred SSR data by key
  • Streamed — Suspense-style boundary
  • ClientOnly — server fallback → client children swap
  • ServerOnly — server children, removed after hydration
  • HttpStatusCode — render-time HTTP status declaration
  • HttpStatusProvider — provides sink to descendant <HttpStatusCode>
  • useDeferred — read deferred Promise by key

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally