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

# Self-hosted apps

> Mount your app in Next.js, Hono, Bun, Deno, Node, Workers or an Effect server, and give it the credentials it needs.

> Reintersect apps are in developer preview. Names and limits can still change before general
> release.

A self-hosted app runs inside a server you operate. Reintersect sends every delivery to one HTTPS
endpoint you declare, and the SDK verifies, routes and answers it. Self-host when the app needs your
own database or network, a fixed outbound address, or a runtime the hosted platform doesn't offer.

## Declare the endpoint

```ts src/reintersect/app.ts theme={null}
import { createReintersectApp } from "@reintersect/sdk/app";
import { TaskBlock } from "./taskBlock";

export const app = createReintersectApp({
  name: "Tasks",
  slug: "tasks",
  hosting: "self",
  endpoint: "https://tasks.example.com/api/reintersect",
}).addBlock(TaskBlock);
```

Reintersect sends `POST` requests to `endpoint` for every block render, action, insert, select
search, reference resolve and search, command, message action and event. The endpoint's path ending in
`/reintersect` is the delivery path; the part before it (`/api` here) is the app's root, where
[routes](/hosting/routes) and the MCP server (`/mcp`) are served. The SDK checks the
[webhook signature](/webhooks-and-security) on every delivery and rejects anything unsigned, stale or
replayed.

Endpoints must be HTTPS on the default port and resolve to public addresses. Reintersect doesn't
follow redirects from them.

## Environment

Set these on your server:

| Variable | Used for |
| - | - |
| `REINTERSECT_APP_SIGNING_SECRET` | Verifying deliveries. From **Settings → Developer → Apps → your app**. |
| `REINTERSECT_APP_CLIENT_ID`, `REINTERSECT_APP_CLIENT_SECRET` | Minting install tokens for `reintersect.forInstall()` and `createReintersectClient(app, { installId })`. |
| `REINTERSECT_BASE_URL` | Optional. Defaults to `https://api.reintersect.com`. |

Also set the variables your app declares in `env`. The SDK reads them from the environment, or from the
`env` object you pass as the second argument of `app.fetch` (as Workers does).

## Mount the app

`app.fetch(request, env?, ctx?)` is a standard fetch handler. It only has a function type once the
app is complete: if a piece needs a block, environment variable or service the app doesn't provide,
the mount is a type error that names what is missing. Mount it so it receives the delivery path, your
routes and `/mcp`.

<Tabs>
  <Tab title="Next.js">
    A catch-all route under `/api`, as the Next.js template writes it:

    ```ts src/app/api/[[...path]]/route.ts theme={null}
    import { app } from "@/reintersect/app";

    const serve = (request: Request) => app.fetch(request);

    export {
      serve as DELETE,
      serve as GET,
      serve as HEAD,
      serve as OPTIONS,
      serve as PATCH,
      serve as POST,
      serve as PUT,
    };
    ```

    The project's JSX is Reintersect JSX; start any React page or component with
    `/** @jsxImportSource react */`.
  </Tab>

  <Tab title="Hono on Bun">
    ```ts src/server.ts theme={null}
    import { Hono } from "hono";
    import { app } from "./app";

    const server = new Hono();

    server.get("/", (context) => context.text("Tasks is running."));
    server.mount("/api", (request) => app.fetch(request));

    Bun.serve({ port: Number(Bun.env.PORT ?? 3000), fetch: server.fetch });
    ```
  </Tab>

  <Tab title="Deno">
    ```ts main.ts theme={null}
    import { app } from "./reintersect/app.ts";

    Deno.serve((request) => app.fetch(request));
    ```
  </Tab>

  <Tab title="Node">
    `toNodeHandler` adapts the app to Node's `http` request listener, so it also works with Express
    and other frameworks built on it. It streams responses, including binary bodies and server-sent
    events, and sends every `set-cookie` header:

    ```ts src/server.ts theme={null}
    import { createServer } from "node:http";
    import { toNodeHandler } from "@reintersect/sdk/app";
    import { app } from "./reintersect/app";

    createServer(toNodeHandler(app)).listen(3000);
    ```
  </Tab>

  <Tab title="Cloudflare Workers">
    In your own Workers account, pass the bindings and execution context through, so `waitUntil` and
    `env` work:

    ```ts src/worker.ts theme={null}
    import { app } from "./reintersect/app";

    export default {
      fetch: (request: Request, env: unknown, ctx: ExecutionContext) => app.fetch(request, env, ctx),
    };
    ```
  </Tab>

  <Tab title="Effect">
    `ReintersectApp.layer` mounts the delivery path, every route and `/mcp` on an `HttpLayerRouter`.
    Services your handlers require flow into the layer's requirements. This is the Effect template's
    server:

    ```ts src/server.ts theme={null}
    import { HttpLayerRouter } from "@effect/platform";
    import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
    import { ReintersectApp } from "@reintersect/sdk/effect/app";
    import { Config, Layer } from "effect";
    import { createServer } from "node:http";
    import { app } from "./app";

    const HttpServerLive = NodeHttpServer.layerConfig(createServer, {
      port: Config.port("PORT").pipe(Config.withDefault(3000)),
    });

    HttpLayerRouter.serve(ReintersectApp.layer(app, { path: "/api/reintersect" })).pipe(
      Layer.provide(HttpServerLive),
      Layer.launch,
      NodeRuntime.runMain,
    );
    ```

    See [Services and layers](/effect/services-and-layers) in the Effect guide.
  </Tab>
</Tabs>

## Install tokens

A self-hosted app keeps only install ids and never stores Reintersect tokens:

* Inside a delivery, the body carries a one-hour install token for that install, and the handler's
  `reintersect` client uses it. Action deliveries also carry an interaction token for the one block,
  valid for 15 minutes.
* Outside a delivery (routes, cron jobs, your own backend), call `reintersect.forInstall(installId)`
  in a route, or `createReintersectClient(app, { installId })` anywhere. The SDK mints a one-hour token
  with your client credentials (`POST /v0/apps/installs/:installId/access-tokens`), caches it and
  renews it before it expires.

Record install ids when the [install link](/concepts/install-and-consent#install-link) redirects
back, or from the `app.installed` event, and forget them on `app.uninstalled`.

<Warning>
  Treat the client secret like a password. With it and an install id, anyone can act as your app in
  that workspace, within the scopes the admin granted. Rotating it in Developer → Apps stops the old
  secret at once.
</Warning>

## Local development

`reintersect dev` builds your app with esbuild, runs it in a local Node server, and relays deliveries
for your development install to it. The relay replays the exact signed request production would send,
so signature checks run locally too. Your endpoint doesn't need to be reachable from the internet
while you develop. Pick the local port with `--port`.

To run your framework's own dev server (`npm run dev:next` in the Next.js template), start it next to
`reintersect dev`; deliveries during development still go to the CLI's local server.

## Deploy

Deploy your server the way you normally do, then sync the manifest so Reintersect knows about new
blocks, actions and scopes:

```bash theme={null}
npx reintersect deploy --app app_123
```

For a self-hosted app this uploads no code. It records a new version with the manifest and endpoint,
and the same [publishing rules](/concepts/install-and-consent#publishing-new-versions) apply. Deploy
your server first, so the endpoint can handle the new version before installs move to it. The
[GitHub Action](/ci) does the sync on every merge.


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