---
name: rtie-module-development
description: Build and publish a custom RTIE module using @rtie/module-sdk — manifest, routes, events, SDK surface, and typed MCP tools.
version: "0.1.0"
---

# Building a RTIE Module

Every RTIE feature is a module. Modules are the unit of deployment, enablement, and marketplace listing.
This skill covers the full module authoring lifecycle.

## Install the module SDK

```bash
bun add @rtie/module-sdk
```

## Module anatomy

A module lives in `modules/<id>/` and has:

```
modules/my-module/
  manifest.ts      # Declares the module's contract
  src/
    routes.ts      # Hono router for the module's HTTP routes
    service.ts     # Business logic (no cross-module SQL)
    types.ts       # Shared types
```

## Writing the manifest

```typescript
// modules/my-module/manifest.ts
import type { ModuleManifest } from "@rtie/module-sdk";

export const manifest: ModuleManifest = {
  id: "my-module",
  name: "My Module",
  version: "1.0.0",
  publisher: "Acme Corp",           // or "RTIE Labs" for first-party
  tier: "marketplace",              // "first-party" | "marketplace"
  accessLevel: "universal",         // "universal" | "vertical-bundle" | "addon" | "platform"
  productKey: null,                 // null for cross-product; "vauction" etc. for product-specific
  description: "Does something useful in real time.",

  routes: [
    { method: "GET",  path: "/v1/my-module/things",     description: "List things" },
    { method: "POST", path: "/v1/my-module/things",     description: "Create a thing" },
    { method: "GET",  path: "/v1/my-module/things/:id", description: "Get a thing" },
  ],

  events: ["my-module.thing.created", "my-module.thing.updated"],

  sdk_surface: ["listThings", "createThing", "getThing"],

  // Typed MCP tool definitions — preferred over bare strings
  mcp_tools: [
    {
      name: "list_things",
      description: "List all things for the tenant.",
      inputSchema: {
        type: "object",
        properties: {
          status: { type: "string", enum: ["active", "archived"] },
        },
      },
      handler: { kind: "http", method: "GET", path: "/v1/my-module/things", query: ["status"] },
      annotations: { readOnlyHint: true },
    },
    {
      name: "create_thing",
      description: "Create a new thing.",
      inputSchema: {
        type: "object",
        properties: {
          name: { type: "string" },
          metadata: { type: "object" },
        },
        required: ["name"],
      },
      handler: { kind: "http", method: "POST", path: "/v1/my-module/things", body: ["name", "metadata"] },
    },
  ],

  // Modules your module depends on (enables them transitively)
  requires: ["auth"],
  conflicts: [],

  // Events your module listens to from other modules
  consumes: ["room.created"],

  // Context Graph contract — required for every module
  contexts: {
    supportedTypes: ["room", "seller_space"],
    resources: ["thing"],
    policyActions: ["my-module.things.read", "my-module.things.write"],
  },
};
```

## Handler kinds

**HTTP handler** (most tools): the MCP worker dispatches the call declaratively.
- `path` supports `{param}` substitution from args; `{tenantId}` is auto-injected
- `query: ["param"]` → appended as query string (GET)
- `body: ["param1", "param2"]` → forwarded as JSON body (POST/PATCH/PUT)

**Custom handler**: for reshaping responses or multi-step logic, register in `apps/mcp/src/worker.ts` CUSTOM_HANDLERS.

## Writing routes

```typescript
// modules/my-module/src/routes.ts
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import type { RtieBindings, HonoVariables } from "@rtie/middleware/hono";
import { getTenantId, getRequestDb } from "@rtie/middleware/hono";

export const myModuleRouter = new OpenAPIHono<{ Bindings: RtieBindings; Variables: HonoVariables }>();

const listThingsRoute = createRoute({
  method: "get", path: "/v1/my-module/things",
  tags: ["My Module"], summary: "List things",
  security: [{ apiKeyAuth: [] }],
  responses: { 200: { description: "Things", content: { "application/json": { schema: z.array(z.object({ id: z.string() })) } } } },
});

myModuleRouter.openapi(listThingsRoute, async (c) => {
  const db = await getRequestDb(c);
  const tenantId = getTenantId(c);
  const things = await db.query.myThings.findMany({ where: (t, { eq }) => eq(t.tenantId, tenantId) });
  return c.json(things);
});
```

## Key rules

1. **Each module owns its tables.** Other modules access your data via your HTTP routes, never by querying your tables directly.
2. **All tables need `context_id`** unless they are intentionally tenant-scoped globals (document the exemption).
3. **Emit events** when state changes. Other modules subscribe to your events, not your DB.
4. **Do not import from another module's internals** — only consume their public routes and events.
5. **`contexts` contract is required** — every manifest must declare `contexts` or the validation will fail.

## Register the module

Add to `modules/_registry/index.ts`:

```typescript
import { manifest as myModule } from "../my-module/manifest.js";
// add to FIRST_PARTY_MODULE_MANIFESTS array
```

Then regenerate the catalog:

```bash
bun modules/_registry/generate.ts
bun apps/mcp/scripts/generate-registry.ts  # if module has mcp_tools
```

## Marketplace publishing

Third-party modules are published to npm with package name `@<publisher>/rtie-module-<id>`.
They implement the same `defineModule()` + manifest shape and are listed in the module catalog.

Full docs: https://docs.rtie.ai/build/overview
