> ## 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.

# Composing an app

> Apps are built from standalone pieces. Each piece declares what it needs, and the app only compiles when every need is met.

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

## Pieces

Everything an app does is a piece you define on its own, usually in its own file:

| Piece | Defined with | Added with |
| - | - | - |
| Block | [`defineBlock`](/blocks/overview) | `.addBlock(Block)` |
| Action | [`defineAction`](/blocks/state-and-actions) | Attached to blocks with `actions: { … }` |
| Reference kind | [`defineReference`](/references/overview) | `.addReference(Reference)` |
| Event handler | [`defineEvent`](/events/overview) | `.addEvent(Handler)` |
| Slash command | [`defineCommand`](/commands) | `.addCommand(Command)` |
| Message action | [`defineMessageAction`](/message-actions) | `.addMessageAction(MessageAction)` |
| HTTP route | [`defineRoute`](/hosting/routes) | `.addRoute(Route)` |
| MCP server | Any fetch-style MCP handler | `.addMcp(handler, { auth })`, see [MCP](/mcp/overview) |

There is no global app object to register handlers on. A piece knows what it needs: the scopes its
code uses, the environment variables it reads, the install config it expects and the blocks it
inserts. The app collects those requirements as you add pieces.

## The app

`createReintersectApp` starts an empty app. Each `add…` call returns a new app with one more piece,
so the order of calls doesn't matter and nothing is mutated:

```ts src/app.ts theme={null}
import { createReintersectApp } from "@reintersect/sdk/app";
import { env, InstallConfig } from "./schemas";
import { TaskBlock } from "./taskBlock";
import { TaskLink } from "./taskReference";

export const app = createReintersectApp({
  name: "Tasks",
  slug: "tasks",
  description: "Tasks from your tracker, live in Reintersect.",
  env,
  installConfig: InstallConfig,
})
  .addBlock(TaskBlock)
  .addReference(TaskLink);
```

### App options

| Option | Type | Description |
| - | - | - |
| `name` | `string` | Display name shown on blocks, the consent screen and the integrations directory. At most 80 characters; it may not start with "Reintersect" or be a role such as "Admin". |
| `slug` | `string` | Unique across Reintersect. 3 to 32 characters from `a-z`, `0-9` and `-`. Defaults to the name in lowercase with dashes. Reserved slugs and slugs starting with `reintersect` are refused. |
| `description` | `string` | One or two sentences for the consent screen, at most 500 characters. |
| `hosting` | `"reintersect" \| "self"` | Defaults to `"reintersect"`. See [Hosting](/hosting/reintersect-hosted). |
| `endpoint` | `string` | Required with `hosting: "self"`: the HTTPS URL Reintersect delivers to, such as `https://example.com/api/reintersect`. |
| `egress` | `string[]` | Hosted apps: hosts your code may call, such as `api.vercel.com` or `*.example.com`. Everything else is blocked. At most 50. |
| `domains` | `string[]` | Domains your [reference patterns](/references/patterns) claim. At most 20. |
| `env` | `Record<string, Schema \| secret()>` | Environment variables and secrets. See [Environment and config](/concepts/env-and-config). |
| `installConfig` | Schema | Settings an admin fills in when installing. |

You upload your app's icon in **Settings → Developer → Apps → your app**. It's the only brand mark
a block shows.

## Requirements checked by TypeScript

`app.fetch` is the HTTP handler Reintersect calls. It only has a function type once every
requirement is met. Until then, its type is an error that names what is missing, so mounting an
incomplete app fails to compile:

```ts theme={null}
import { createReintersectApp } from "@reintersect/sdk/app";
import { RequestApproval } from "./requestApproval";

const incomplete = createReintersectApp({ name: "Expenses" }).addMessageAction(RequestApproval);

export const POST = incomplete.fetch;
// Type error: This app uses pieces it does not include. Add them first: block:expense-approval
```

The checks cover:

| Problem | Message |
| - | - |
| A piece inserts or renders a block the app doesn't include | `This app uses pieces it does not include. Add them first: block:expense-approval` |
| Two pieces share a block type, reference kind, message action id or command name | `… is already part of this app: block types, reference kinds, message actions and commands must be unique` |
| A block was added before it has a view | `Block "go-no-go" has no view: call .render() before adding it` |
| A piece reads an environment variable the app doesn't declare | `This piece reads env.VERCEL_TOKEN, which the app's env does not declare` |
| A piece reads install config the app doesn't declare | `This piece reads config.team, which the app's installConfig does not declare` |
| An action's state doesn't fit a block it's attached to | `Action "abort" writes state.status values this block's state does not allow` |
| A route uses a scope no other piece declares | `Routes never widen the app's scopes, and no other piece declares: …` |
| Pieces from `@reintersect/sdk/app` mixed into an Effect app, or the reverse | `This app comes from @reintersect/sdk/app: define its pieces with @reintersect/sdk/app too` |
| An Effect piece needs a service nobody provides | The missing services, by type |
| `addMcp` called twice | `This app already serves an MCP server at /mcp` |

The same checks run at runtime when you call `add…`, so plain JavaScript gets a clear error too.

### Blocks inside other pieces

A command, message action, event handler, route or reference can produce or update a block. Use the
block's `insert` function, or list it in the piece's `blocks` option, and the requirement travels in
the types:

```tsx theme={null}
import { defineMessageAction } from "@reintersect/sdk/app";
import { GoNoGo } from "./goNoGo";

export const StartGoNoGo = defineMessageAction(
  "start-go-no-go",
  { label: "Start a go/no-go" },
  ({ message }) => ({ visibility: "public", block: GoNoGo.insert({ title: message.text }) }),
);
```

You can also write the answer's block as JSX, such as `<GoNoGo title="Ship v2" />`. JSX erases
to a generic element type, so TypeScript can't record that requirement. `reintersect check` scans
for it (`jsx-block-unregistered`), and the SDK rejects an unregistered block type at runtime. Prefer
`insert` when you can.

## Scopes

Scopes come from the pieces. A piece lists the scopes its own client calls need, and the app's
manifest scopes are the union of its pieces' scopes plus the read scopes of the events it handles.
Calling a client method whose scope the piece didn't declare is a type error:

```ts theme={null}
export const OnDecision = defineEvent("decision.published", async ({ decision, reintersect }) => {
  await reintersect.replies.create({ conversationId: decision.conversationId, content: "Noted." });
  //                    ^ Type error: replies.create requires the "replies:write" scope
});
```

See [Scopes](/reference/scopes).

## Schemas

Props, state, args, input, env values and install config take any schema that implements both
[Standard Schema](https://standardschema.dev) and Standard JSON Schema, because Reintersect needs the
JSON Schema for forms, agents and validation:

| Library | How |
| - | - |
| Zod 4.2 or later | Use `z.object(…)` directly. The templates' default. |
| ArkType 2.1.28 or later | Use `type(…)` directly. |
| Valibot 1 | Wrap each schema with `toStandardJsonSchema()` from `@valibot/to-json-schema`. |
| Effect Schema | Use the [Effect entry](/effect), `@reintersect/sdk/effect/app`. |

A schema without JSON Schema support is a type error that says how to fix it. Recursive schemas are
rejected when the manifest syncs, and each schema must stay within the
[JSON Schema budget](/blocks/limits#manifest).

## Export the app by name

Export the composed app as a named constant called `app`:

```ts theme={null}
export const app = createReintersectApp({ name: "Tasks" }).addBlock(TaskBlock);
```

The CLI loads `src/app.ts` (or `src/app.tsx`, `src/app.js`, `app.ts`, …) unless you pass `--entry` or
set `"reintersect": { "entry": "…" }` in `package.json`. For hosted apps the CLI generates the
Worker entry that imports `{ app }`. A default export also loads, but the CLI tells you to switch to
the named export, because a bare default export is never checked against anything.

## Hosted or mounted

You can host the same `app` value on Reintersect or mount it in your own server:

<Tabs>
  <Tab title="Reintersect-hosted">
    Reintersect builds and runs the app for you. You export `app` and deploy:

    ```bash theme={null}
    npx reintersect deploy
    ```

    See [Reintersect-hosted apps](/hosting/reintersect-hosted).
  </Tab>

  <Tab title="Self-hosted">
    Mount `app.fetch` in your own server and set `hosting: "self"` and `endpoint`:

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

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

    export { serve as GET, serve as POST };
    ```

    `app.fetch(request, env?, ctx?)` works in Next.js, Hono, Bun, Deno and Workers;
    `toNodeHandler(app)` adapts it for Node's `http` and Express; `ReintersectApp.layer(app)` mounts it
    on an Effect `HttpLayerRouter`. See [Self-hosted apps](/hosting/self-hosted).
  </Tab>
</Tabs>

## Sharing actions

Actions are standalone too. The same action can back a button on several blocks. When an action
reads or writes block state, it declares the slice it needs, and `defineBlock` checks that slice
against the block's own state:

```ts theme={null}
import { defineAction } from "@reintersect/sdk/app";
import { z } from "zod";

export const abort = defineAction(
  "abort",
  { state: z.object({ status: z.enum(["open", "shipped", "aborted"]) }) },
  async () => ({ state: { status: "aborted" } }),
);
```

An action without a state slice is stateless. Agents run actions through the blocks they are
rendered in; see [Agents](/agents#running-actions). To run shared logic from a command or a message
action, call the same function from both handlers.


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