> ## Documentation Index
> Fetch the complete documentation index at: https://tailorkit.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate Patchsmith

> Install Patchsmith, expose your host API, and render installed apps in React.

Connect your host product to Patchsmith, define the capabilities apps can use, and render installed apps in React. You need an existing React frontend, a backend that handles standard `Request` and `Response` objects, and a [Patchsmith account](https://patchsmith.io/signup).

Get your project key from Patchsmith and make it available to your server as `PATCHSMITH_PROJECT_KEY`. Keep it out of client bundles. The examples use `@/` for your source directory; adjust imports to match your project.

<Steps titleSize="h2">
  <Step title="Install Patchsmith">
    Choose a validation library to use with Patchsmith. If you’re unsure, use Zod. Then add Patchsmith and your chosen library to your project:

    <Tabs>
      <Tab title="Zod">
        <CodeGroup>
          ```sh npm theme={null}
          npm install patchsmith@beta zod
          ```

          ```sh pnpm theme={null}
          pnpm add patchsmith@beta zod
          ```

          ```sh yarn theme={null}
          yarn add patchsmith@beta zod
          ```

          ```sh bun theme={null}
          bun add patchsmith@beta zod
          ```
        </CodeGroup>
      </Tab>

      <Tab title="ArkType">
        <CodeGroup>
          ```sh npm theme={null}
          npm install patchsmith@beta arktype
          ```

          ```sh pnpm theme={null}
          pnpm add patchsmith@beta arktype
          ```

          ```sh yarn theme={null}
          yarn add patchsmith@beta arktype
          ```

          ```sh bun theme={null}
          bun add patchsmith@beta arktype
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Valibot">
        <CodeGroup>
          ```sh npm theme={null}
          npm install patchsmith@beta valibot @valibot/to-json-schema
          ```

          ```sh pnpm theme={null}
          pnpm add patchsmith@beta valibot @valibot/to-json-schema
          ```

          ```sh yarn theme={null}
          yarn add patchsmith@beta valibot @valibot/to-json-schema
          ```

          ```sh bun theme={null}
          bun add patchsmith@beta valibot @valibot/to-json-schema
          ```
        </CodeGroup>
      </Tab>
    </Tabs>
  </Step>

  <Step title="Define the shared contract">
    Create `src/lib/patchsmith.ts` to define the contract between installed apps and your platform. It declares [components](/docs/concepts/components), [views](/docs/concepts/views), [slots](/docs/concepts/slots), and scopes that identify an installation’s owner. Both your server and frontend import this file, so keep server credentials and logic elsewhere.

    <Tip>
      Breaking contract changes, like removing a component or requiring a field, can
      break existing apps.
    </Tip>

    <Tabs>
      <Tab title="Zod">
        ```ts src/lib/patchsmith.ts theme={null}
        import { defineContract, type Component } from "patchsmith";
        import { primitives } from "patchsmith/zod";
        import { z } from "zod";

        // Define the components you want apps to use
        const Button = {
          fields: z.object({
            variant: z.enum(["default", "secondary"]),
          }),
          callbacks: {
            onClick: {},
          },
          children: true,
        } satisfies Component;

        const Input = {
          fields: z.object({ value: z.string() }),
          callbacks: {
            onValueChange: {
              input: z.object({ value: z.string() }),
            },
          },
        } satisfies Component;

        export const contract = defineContract({
          // Values form the installation key, e.g. userId + teamId scopes installs per user per team.
          scopes: { user: z.object({ userId: z.string().min(1) }) },

          components: {
            // These give apps basic layout and styling components; we strongly recommend including them.
            ...primitives({
              // Customise these colors to match your app's design tokens.
              tokens: {
                borderColor: {
                  default: "var(--border)",
                  input: "var(--input)",
                  destructive: "var(--destructive)",
                },
                textColor: {
                  default: "var(--foreground)",
                  muted: "var(--muted-foreground)",
                  primary: "var(--primary)",
                  destructive: "var(--destructive)",
                },
              },
            }),
            Button,
            Input,
          },

          // Views share page data with apps, which can provide UI for each view.
          // Patchsmith shows the closest match, falling back to "/" if the app provides it.
          views: {
            "/": z.object({
              user: z.object({ id: z.string(), name: z.string() }),
            }),
          },

          // Slots define where apps render and which views they support.
          slots: {
            page: {
              views: ["/"],
              description:
                "Full screen content accessed via the sidebar. Width and height vary by device; use responsive layouts.",
            },
          },
        });
        ```
      </Tab>

      <Tab title="ArkType">
        ```ts src/lib/patchsmith.ts theme={null}
        import { defineContract, type Component } from "patchsmith";
        import { primitives } from "patchsmith/arktype";
        import { type } from "arktype";

        // Define the components you want apps to use
        const Button = {
          fields: type({
            variant: "'default' | 'secondary'",
          }),
          callbacks: {
            onClick: {},
          },
          children: true,
        } satisfies Component;

        const Input = {
          fields: type({ value: "string" }),
          callbacks: {
            onValueChange: {
              input: type({ value: "string" }),
            },
          },
        } satisfies Component;

        export const contract = defineContract({
          // Values form the installation key, e.g. userId + teamId scopes installs per user per team.
          scopes: {
            user: type({ userId: "string > 0" }),
          },

          components: {
            // These give apps basic layout and styling components; we strongly recommend including them.
            ...primitives({
              // Customise these colors to match your app's design tokens.
              tokens: {
                borderColor: {
                  default: "var(--border)",
                  input: "var(--input)",
                  destructive: "var(--destructive)",
                },
                textColor: {
                  default: "var(--foreground)",
                  muted: "var(--muted-foreground)",
                  primary: "var(--primary)",
                  destructive: "var(--destructive)",
                },
              },
            }),
            Button,
            Input,
          },

          // Views share page data with apps, which can provide UI for each view.
          // Patchsmith shows the closest match, falling back to "/" if the app provides it.
          views: {
            "/": type({
              user: { id: "string", name: "string" },
            }),
          },

          // Slots define where apps render and which views they support.
          slots: {
            page: {
              views: ["/"],
              description:
                "Full screen content accessed via the sidebar. Width and height vary by device; use responsive layouts.",
            },
          },
        });
        ```
      </Tab>

      <Tab title="Valibot">
        ```ts src/lib/patchsmith.ts theme={null}
        import { defineContract, type Component } from "patchsmith";
        import { primitives } from "patchsmith/valibot";
        import * as v from "valibot";

        // Define the components you want apps to use
        const Button = {
          fields: v.object({
            variant: v.picklist(["default", "secondary"]),
          }),
          callbacks: {
            onClick: {},
          },
          children: true,
        } satisfies Component;

        const Input = {
          fields: v.object({ value: v.string() }),
          callbacks: {
            onValueChange: {
              input: v.object({ value: v.string() }),
            },
          },
        } satisfies Component;

        export const contract = defineContract({
          // Values form the installation key, e.g. userId + teamId scopes installs per user per team.
          scopes: { user: v.object({ userId: v.pipe(v.string(), v.minLength(1)) }) },

          components: {
            // These give apps basic layout and styling components; we strongly recommend including them.
            ...primitives({
              // Customise these colors to match your app's design tokens.
              tokens: {
                borderColor: {
                  default: "var(--border)",
                  input: "var(--input)",
                  destructive: "var(--destructive)",
                },
                textColor: {
                  default: "var(--foreground)",
                  muted: "var(--muted-foreground)",
                  primary: "var(--primary)",
                  destructive: "var(--destructive)",
                },
              },
            }),
            Button,
            Input,
          },

          // Views are URL-like paths for your product's context; apps can provide UI for each.
          // Patchsmith shows the closest match, checking parent paths up to "/".
          views: {
            "/": v.object({
              user: v.object({ id: v.string(), name: v.string() }),
            }),
          },

          // Slots define where apps render and which views they support.
          slots: {
            page: {
              views: ["/"],
              description:
                "Full screen content accessed via the sidebar. Width and height vary by device; use responsive layouts.",
            },
          },
        });
        ```
      </Tab>
    </Tabs>

    A slot's optional `description` gives app authors and agents layout and sizing guidance. Your host controls the actual dimensions. See [Slots](/docs/concepts/slots#describe-the-space) for a side panel example.
  </Step>

  <Step title="Implement the server">
    Create `src/lib/patchsmith-server.ts` to connect your contract to Patchsmith using your project key. Set `baseUrl` to your host’s Patchsmith handler URL in each environment, including production.

    <Tabs>
      <Tab title="Zod">
        ```ts src/lib/patchsmith-server.ts theme={null}
        import { createServer } from "patchsmith/server";
        import { contract } from "./patchsmith";

        export const patchSmith = createServer({
          contract,
          projectKey: process.env.PATCHSMITH_PROJECT_KEY,
          baseUrl:
            process.env.NODE_ENV === "development"
              ? "http://localhost:3000/api/patchsmith"
              : "https://your-app.com/api/patchsmith",
        });
        ```
      </Tab>

      <Tab title="ArkType">
        ```ts src/lib/patchsmith-server.ts theme={null}
        import { createServer } from "patchsmith/server";
        import { contract } from "./patchsmith";

        export const patchSmith = createServer({
          contract,
          projectKey: process.env.PATCHSMITH_PROJECT_KEY,
          baseUrl:
            process.env.NODE_ENV === "development"
              ? "http://localhost:3000/api/patchsmith"
              : "https://your-app.com/api/patchsmith",
        });
        ```
      </Tab>

      <Tab title="Valibot">
        ```ts src/lib/patchsmith-server.ts theme={null}
        import { createServer } from "patchsmith/server";
        import { toJsonSchema } from "@valibot/to-json-schema";
        import { contract } from "./patchsmith";

        export const patchSmith = createServer({
          contract,
          projectKey: process.env.PATCHSMITH_PROJECT_KEY,
          baseUrl:
            process.env.NODE_ENV === "development"
              ? "http://localhost:3000/api/patchsmith"
              : "https://your-app.com/api/patchsmith",
          schemaSerializer: toJsonSchema,
        });
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Create the client">
    Create `src/lib/patchsmith-client.tsx` to connect your frontend to the Patchsmith API. Map each contract component to its React implementation. The example assumes your `Button` and `Input` components accept the props shown below.

    ```tsx src/lib/patchsmith-client.tsx theme={null}
    "use client";

    import { Button } from "@/components/button";
    import { Input } from "@/components/input";
    import { contract } from "@/lib/patchsmith";
    import { createClient, primitives } from "patchsmith/react";

    export const patchSmithClient = createClient({
      contract,
      // Replace this with your host origin and handler path.
      baseUrl:
        process.env.NODE_ENV === "development"
          ? "http://localhost:3000/api/patchsmith"
          : "https://your-app.com/api/patchsmith",
      components: {
        ...primitives,

        Button: ({ props, children }) => (
          <Button variant={props.variant} onClick={props.onClick}>
            {children}
          </Button>
        ),

        Input: ({ props }) => (
          <Input
            value={props.value}
            onChange={(event) => props.onValueChange({ value: event.target.value })}
          />
        ),
      },
    });

    export const {
      Provider: PatchsmithProvider,
      RenderSlot,
      useApps,
      useViewContext,
      useViews,
    } = patchSmithClient;

    export default patchSmithClient;
    ```
  </Step>

  <Step title="Add the API handler">
    Mount the handler at your Patchsmith base path. If you use a path other than `/api/patchsmith`, update `baseUrl` in both `patchsmith-server.ts` and `patchsmith-client.tsx` to match.

    Choose your framework below. Replace `auth` with your authentication implementation and return a stable `subjectId` and verified scope values from the session.

    <Tabs>
      <Tab title="Next.js App Router">
        ```ts app/api/patchsmith/[...patchsmith]/route.ts theme={null}
        import { patchSmith } from "@/lib/patchsmith-server";

        async function handleRequest(request: Request) {
          return patchSmith.handler(request, {
            authenticate: async () => {
              // Replace this with your own authentication logic.
              const session = await auth(request);

              if (!session) {
                return null;
              }

              return {
                subjectId: session.user.id,
                scopes: { user: { userId: session.user.id } },
              };
            },
          });
        }

        export const GET = handleRequest;
        export const POST = handleRequest;
        ```
      </Tab>

      <Tab title="TanStack Start">
        ```ts routes/api/patchsmith.$.ts theme={null}
        import { patchSmith } from "@/lib/patchsmith-server";
        import { createFileRoute } from "@tanstack/react-router";

        export const Route = createFileRoute("/api/patchsmith/$")({
          server: {
            handlers: {
              GET: handleRequest,
              POST: handleRequest,
            },
          },
        });

        async function handleRequest({ request }: { request: Request }) {
          return patchSmith.handler(request, {
            authenticate: async () => {
              // Replace this with your own authentication logic.
              const session = await auth(request);

              if (!session) {
                return null;
              }

              return {
                subjectId: session.user.id,
                scopes: { user: { userId: session.user.id } },
              };
            },
          });
        }
        ```
      </Tab>

      <Tab title="React Router">
        ```ts routes/api.patchsmith.$.ts theme={null}
        import { patchSmith } from "@/lib/patchsmith-server";
        import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";

        async function handleRequest(request: Request) {
          return patchSmith.handler(request, {
            authenticate: async () => {
              // Replace this with your own authentication logic.
              const session = await auth(request);

              if (!session) {
                return null;
              }

              return {
                subjectId: session.user.id,
                scopes: { user: { userId: session.user.id } },
              };
            },
          });
        }

        export async function loader({ request }: LoaderFunctionArgs) {
          return handleRequest(request);
        }

        export async function action({ request }: ActionFunctionArgs) {
          return handleRequest(request);
        }
        ```
      </Tab>

      <Tab title="Nitro">
        ```ts server/api/patchsmith/[...].ts theme={null}
        import { patchSmith } from "@/lib/patchsmith-server";
        import { toWebRequest } from "h3";

        export default defineEventHandler(async (event) => {
          const request = toWebRequest(event);
          return patchSmith.handler(request, {
            authenticate: async () => {
              // Replace this with your own authentication logic.
              const session = await auth(request);

              if (!session) {
                return null;
              }

              return {
                subjectId: session.user.id,
                scopes: { user: { userId: session.user.id } },
              };
            },
          });
        });
        ```
      </Tab>

      <Tab title="Hono">
        ```ts src/index.ts theme={null}
        import { Hono } from "hono";
        import { patchSmith } from "@/lib/patchsmith-server";

        const app = new Hono();

        app.on(["GET", "POST"], "/api/patchsmith/*", async (c) => {
          return patchSmith.handler(c.req.raw, {
            authenticate: async () => {
              // Replace this with your own authentication logic.
              const session = await auth(c.req.raw);

              if (!session) {
                return null;
              }

              return {
                subjectId: session.user.id,
                scopes: { user: { userId: session.user.id } },
              };
            },
          });
        });

        export default app;
        ```
      </Tab>

      <Tab title="Other">
        Pass your framework's `Request` to this handler and return its `Response`.

        ```ts src/lib/patchsmith-handler.ts theme={null}
        import { patchSmith } from "@/lib/patchsmith-server";

        export async function handleRequest(request: Request): Promise<Response> {
          return patchSmith.handler(request, {
            authenticate: async () => {
              // Replace this with your own authentication logic.
              const session = await auth(request);

              if (!session) {
                return null;
              }

              return {
                subjectId: session.user.id,
                scopes: { user: { userId: session.user.id } },
              };
            },
          });
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Add the provider">
    Wrap pages that use Patchsmith hooks or render apps in `PatchsmithProvider`. Choose the root layout for your framework:

    <Tabs>
      <Tab title="Next.js App Router">
        ```tsx app/layout.tsx theme={null}
        import type { ReactNode } from "react";
        import { PatchsmithProvider } from "@/lib/patchsmith-client";

        export default function RootLayout({ children }: { children: ReactNode }) {
          return (
            <html lang="en">
              <body>
                <PatchsmithProvider>{children}</PatchsmithProvider>
              </body>
            </html>
          );
        }
        ```
      </Tab>

      <Tab title="TanStack Start">
        ```tsx src/routes/__root.tsx theme={null}
        import type { ReactNode } from "react";
        import {
          createRootRoute,
          HeadContent,
          Outlet,
          Scripts,
        } from "@tanstack/react-router";
        import { PatchsmithProvider } from "@/lib/patchsmith-client";

        export const Route = createRootRoute({
          shellComponent: RootDocument,
          component: Outlet,
        });

        function RootDocument({ children }: { children: ReactNode }) {
          return (
            <html lang="en">
              <head>
                <HeadContent />
              </head>
              <body>
                <PatchsmithProvider>{children}</PatchsmithProvider>
                <Scripts />
              </body>
            </html>
          );
        }
        ```
      </Tab>

      <Tab title="React Router">
        ```tsx app/root.tsx theme={null}
        import type { ReactNode } from "react";
        import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
        import { PatchsmithProvider } from "@/lib/patchsmith-client";

        export function Layout({ children }: { children: ReactNode }) {
          return (
            <html lang="en">
              <head>
                <meta charSet="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <Meta />
                <Links />
              </head>
              <body>
                {children}
                <ScrollRestoration />
                <Scripts />
              </body>
            </html>
          );
        }

        export default function App() {
          return (
            <PatchsmithProvider>
              <Outlet />
            </PatchsmithProvider>
          );
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Render a view">
    Use `useViewContext` to share page data with apps and `useViews` to find apps available in the `page` slot. Render the selected app with `RenderSlot`.

    <Tabs>
      <Tab title="Next.js App Router">
        ```tsx app/app-views/page.tsx theme={null}
        "use client";

        import { useState } from "react";
        import { RenderSlot, useViews, useViewContext } from "@/lib/patchsmith-client";

        export default function Page() {
          const [selectedId, setSelectedId] = useState("");
          const { data: views, isPending, error } = useViews({ slot: "page" });
          // Replace this with your own authentication hook.
          const { data: session, isPending: isSessionPending } = useSession();

          useViewContext(
            "/",
            session
              ? { context: { user: session.user } }
              : { context: undefined, loading: true }
          );

          const selectedView =
            views?.find((view) => view.app.id === selectedId) ?? views?.[0];

          if (isPending || isSessionPending) return <p>Loading views…</p>;
          if (error) return "An error has occurred: " + error.message;
          if (!selectedView) return <p>No views available yet.</p>;

          return (
            <>
              <label>
                View
                <select
                  value={selectedView.app.id}
                  onChange={(event) => setSelectedId(event.target.value)}
                >
                  {views?.map((view) => (
                    <option key={view.app.id} value={view.app.id}>
                      {view.app.name ?? view.app.id}
                    </option>
                  ))}
                </select>
              </label>

              <RenderSlot
                key={selectedView.app.id}
                app={selectedView.app}
                slot="page"
              />
            </>
          );
        }
        ```
      </Tab>

      <Tab title="TanStack Start">
        ```tsx src/routes/app-views.tsx theme={null}
        import { createFileRoute } from "@tanstack/react-router";
        import { useState } from "react";
        import { RenderSlot, useViews, useViewContext } from "@/lib/patchsmith-client";

        export const Route = createFileRoute("/app-views")({ component: Page });

        function Page() {
          const [selectedId, setSelectedId] = useState("");
          const { data: views, isPending, error } = useViews({ slot: "page" });
          // Replace this with your own authentication hook.
          const { data: session, isPending: isSessionPending } = useSession();

          useViewContext(
            "/",
            session
              ? { context: { user: session.user } }
              : { context: undefined, loading: true }
          );

          const selectedView =
            views?.find((view) => view.app.id === selectedId) ?? views?.[0];

          if (isPending || isSessionPending) return <p>Loading views…</p>;
          if (error) return "An error has occurred: " + error.message;
          if (!selectedView) return <p>No views available yet.</p>;

          return (
            <>
              <label>
                View
                <select
                  value={selectedView.app.id}
                  onChange={(event) => setSelectedId(event.target.value)}
                >
                  {views?.map((view) => (
                    <option key={view.app.id} value={view.app.id}>
                      {view.app.name ?? view.app.id}
                    </option>
                  ))}
                </select>
              </label>

              <RenderSlot
                key={selectedView.app.id}
                app={selectedView.app}
                slot="page"
              />
            </>
          );
        }
        ```
      </Tab>

      <Tab title="React Router">
        ```tsx app/routes/app-views.tsx theme={null}
        import { useState } from "react";
        import { RenderSlot, useViews, useViewContext } from "@/lib/patchsmith-client";

        export default function Page() {
          const [selectedId, setSelectedId] = useState("");
          const { data: views, isPending, error } = useViews({ slot: "page" });
          // Replace this with your own authentication hook.
          const { data: session, isPending: isSessionPending } = useSession();

          useViewContext(
            "/",
            session
              ? { context: { user: session.user } }
              : { context: undefined, loading: true }
          );

          const selectedView =
            views?.find((view) => view.app.id === selectedId) ?? views?.[0];

          if (isPending || isSessionPending) return <p>Loading views…</p>;
          if (error) return "An error has occurred: " + error.message;
          if (!selectedView) return <p>No views available yet.</p>;

          return (
            <>
              <label>
                View
                <select
                  value={selectedView.app.id}
                  onChange={(event) => setSelectedId(event.target.value)}
                >
                  {views?.map((view) => (
                    <option key={view.app.id} value={view.app.id}>
                      {view.app.name ?? view.app.id}
                    </option>
                  ))}
                </select>
              </label>

              <RenderSlot
                key={selectedView.app.id}
                app={selectedView.app}
                slot="page"
              />
            </>
          );
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Verify the integration">
    Start your host app, then check these endpoints at your configured host origin:

    ```txt theme={null}
    GET /api/patchsmith/schema
    GET /api/patchsmith/meta
    GET /api/patchsmith/apps
    ```

    The schema and metadata endpoints describe your public app contract. The apps endpoint requires a valid host session and returns the apps installed for the authenticated scopes. Each scope is named by the schema key, such as `user`.

    While signed in to your host, open `/app-views`. You should see either an app view or “No views available yet.” An unauthenticated request to `/api/patchsmith/apps` should be rejected.

    If the routes return 404, check the catch-all route and handler path. If the schema works but apps fail to load, check that `authenticate` returns a valid session and that the client points to the same origin as your host.
  </Step>
</Steps>

## Create your first app

Follow [Create an app](/docs/writing-apps/overview) to scaffold an app against your host contract. Then [write a view](/docs/writing-apps/ui) for one of its slots.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.