---
name: plugin-client
description: Connect to and consume deployed everything.dev plugins from an external app, child project, or script. Use when creating API/auth clients, reading runtime config, authenticating with API keys or sessions, or calling plugin routes programmatically.
metadata:
  sources: "ui/src/lib/api.ts,ui/src/lib/auth.ts,ui/src/app.ts,everything-dev/src/ui/runtime.ts,everything-dev/src/types.ts,host/src/program.ts,host/src/services/auth.ts,api/src/lib/auth.ts"
---

# Plugin Client — Consuming everything.dev APIs

## Minimal Connection Config

Every everything.dev host exposes two surfaces:

| Surface | URL | Purpose |
|---------|-----|---------|
| RPC | `{hostUrl}/api/rpc` | Typed oRPC procedure calls (POST) |
| OpenAPI / REST | `{hostUrl}/api` | Scalar docs + OpenAPI spec + REST routes |

The host injects a `ClientRuntimeConfig` into `window.__RUNTIME_CONFIG__` via an inline script during SSR. The key fields for client connection:

```ts
{
  hostUrl: "https://dev.everything.dev",  // the host server origin
  apiBase: "/api",
  rpcBase: "/api/rpc",
  authAvailable: true,
  account: "dev.everything.near",
  networkId: "mainnet",
}
```

Read it with `getRuntimeConfig()` from `everything-dev/ui/runtime`:

```ts
import { getRuntimeConfig } from "everything-dev/ui/runtime";

const config = getRuntimeConfig();  // throws if window.__RUNTIME_CONFIG__ is missing
```

Or use the `@/app` helpers (in-repo UI only):

```ts
import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app";

const account = getAccount();                          // "dev.everything.near"
const runtime = getActiveRuntime();                    // { accountId, gatewayId, title, ... }
const appName = getAppName();                           // runtime title or account
```

All helpers accept an optional config argument for SSR — pass `runtimeConfig` from loader data instead of reading `window`.

## External / Standalone API Client

An external app, script, or child project can connect with just `@orpc/client` and `@orpc/client/fetch`:

```ts
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";

const HOST = "https://dev.everything.dev";

const link = new RPCLink({
  url: `${HOST}/api/rpc`,
  fetch: globalThis.fetch,
  headers: {
    "x-api-key": process.env.EVERYTHING_DEV_API_KEY!,
  },
});

const client = createORPCClient(link);
```

### URL Structure

oRPC's RPC protocol encodes the procedure path in the URL segment after the RPC base:

| Call | HTTP Request |
|------|-------------|
| `client.ping()` | `POST {hostUrl}/api/rpc/ping` |
| `client.apps.listRegistryApps({ limit: 24 })` | `POST {hostUrl}/api/rpc/apps/listRegistryApps` |
| `client.auth.getSession()` | `POST {hostUrl}/api/rpc/auth/getSession` |

The host matches `/api/rpc/{pluginKey}/*` prefixes and dispatches to each plugin's `RPCHandler`. Base API procedures (not namespaced) go to the main API handler at `/api/rpc`.

### Plugin Namespacing

Plugins are namespaced by their `bos.config.json` key. If the config has:

```json
{
  "plugins": {
    "apps": { "production": "https://..." },
    "template": { "production": "https://..." }
  }
}
```

Then the client surface is:

```ts
client.ping();                      // base API
client.apps.listRegistryApps();     // "apps" plugin
client.template.someProcedure();    // "template" plugin
client.auth.getSession();           // auth plugin (mounted at /api/rpc/auth)
```

Without the generated `ApiContract` type, the client is untyped — but the runtime behavior is identical. Use OpenAPI codegen (see below) for typed clients in other languages.

### Auth for External Clients

Two credential mechanisms:

**API key** — pass `x-api-key` header on every request:

```ts
const link = new RPCLink({
  url: `${HOST}/api/rpc`,
  fetch: globalThis.fetch,
  headers: { "x-api-key": "edk_xxxxxxxxxxxxxxx" },
});
```

**Session cookies** — use `credentials: "include"` (browser only, requires CORS origin allowlist):

```ts
const link = new RPCLink({
  url: `${HOST}/api/rpc`,
  fetch: (url, options) => fetch(url, { ...options, credentials: "include" }),
});
```

The host's session middleware resolves auth on every `/api/*` request by calling Better-Auth's `getContext()`, which reads session cookies and the `x-api-key` header. The resulting context (user, apiKey, organization) is forwarded into every plugin's oRPC handler.

## In-Repo API Client

Within the everything.dev UI, use `createApiClient` from `@/app`:

```ts
import { createApiClient, useApiClient, useOrpc } from "@/app";

// In hydration (once):
const apiClient = createApiClient({
  hostUrl: runtimeConfig.hostUrl,
  rpcBase: runtimeConfig.rpcBase,
});

// In route components:
const apiClient = useApiClient();
const result = await apiClient.apps.listRegistryApps({ limit: 24 });

// With TanStack Query utils:
const orpc = useOrpc();
const { data } = orpc.apps.listRegistryApps.useQuery({ limit: 24 });
```

`createApiClient` caches a singleton in the browser (when no SSR headers are passed). SSR builds a fresh client per request with forwarded cookies. See the `ui-integration` skill for route-level patterns, loaders, and error handling.

## Auth Client (Session-Based)

The auth client uses Better-Auth with SIWN (NEAR wallet sign-in). Create it with `createAuthClient` from `@/app`:

```ts
import { createAuthClient, sessionQueryOptions, useAuthClient } from "@/app";

// In hydration:
const authClient = createAuthClient({ runtimeConfig, cspNonce });

// In route components:
const authClient = useAuthClient();
```

### SIWN Configuration

The auth client reads NEAR recipient config from `runtimeConfig.auth.variables.siwn`:

```ts
// runtimeConfig.auth.variables.siwn:
{
  recipient: "dev.everything.near",           // single network
  // OR:
  recipients: {
    mainnet: "dev.everything.near",
    testnet: "dev.testnet.near",
  },
}
```

The client resolves `mainnetRecipient` from `recipients.mainnet ?? recipient` and infers `networkId` from the runtime config or the recipient suffix.

### Sign-In Flows

```ts
// NEAR wallet (SIWN)
await authClient.signIn.siwn({ networkId: "mainnet" });

// Email/password
await authClient.signIn.email({ email, password });

// Passkey
await authClient.signIn.passkey();

// Sign out
await authClient.signOut();

// Organization switching
await authClient.organization.setActive({ organizationId: "org_123" });
```

### Session Query

```ts
import { sessionQueryOptions } from "@/app";

// In a route loader (SSR-safe with initialData):
const session = await queryClient.ensureQueryData(
  sessionQueryOptions(authClient, context.session),
);

// In a component:
const { data: session } = useQuery(sessionQueryOptions(authClient));
```

Session query key is `["session"]` with 1-min stale time and 10-min GC time.

### SSR Cookie Forwarding

On the server, cookies can't be read from `window`. Pass request headers:

```ts
const authClient = createAuthClient({
  runtimeConfig,
  headers: request.headers,    // forwards session cookies
  cspNonce,
});
```

Both API and auth clients use `credentials: "include"` unconditionally. The host's CORS allows any HTTPS origin in production (configurable via `CORS_ORIGIN`).

## API Key Auth

For programmatic access (scripts, backend services, CI), use API keys instead of sessions.

### Creating API Keys

API keys are created via the Better-Auth `apiKeyClient()` plugin (exposed as `authClient.apiKey.*`):

```ts
const authClient = useAuthClient();

// Create a key (secret returned once!)
const { data, error } = await authClient.apiKey.create({
  name: "my-bot-key",
  configId: "org-keys",           // optional: scope to a key config
  organizationId: "org_123",      // optional: scope to an org
  expiresIn: 60 * 60 * 24 * 30,   // optional: 30 days (seconds)
});

if (data) {
  console.log(data.apiKey.key);   // "edk_xxxxxxxxx" — save this now
}
```

The full key secret is only returned at creation time. Store it immediately.

### Using API Keys

Pass the key in the `x-api-key` header:

```ts
const link = new RPCLink({
  url: `${HOST}/api/rpc`,
  fetch: globalThis.fetch,
  headers: { "x-api-key": "edk_xxxxxxxxxxxxxxx" },
});
```

Or with plain fetch to REST endpoints:

```ts
const res = await fetch(`${HOST}/api/v1/registry/apps`, {
  headers: { "x-api-key": "edk_xxxxxxxxxxxxxxx" },
});
```

### Permission Scopes

Keys have a `permissions` map (`Record<string, string[]>` — resource to allowed actions). Routes protected by `requireApiKey(requiredPermissions)` check that the key's permissions cover all required resource/action pairs:

```ts
// Server-side (plugin route):
builder.deleteThing
  .use(requireApiKey({ things: ["delete"] }))
  .handler(async ({ context }) => {
    context.apiKey.permissions;  // { things: ["delete", "create"], ... }
  });
```

If the key lacks a required permission, the server returns `FORBIDDEN` with `{ requiredPermissions, keyPermissions }` in the error data.

### Dual-Mode Auth

`requireAuthOrApiKey` accepts either a session or an API key — useful for routes that serve both interactive UI and programmatic clients:

```ts
builder.createThing
  .use(requireAuthOrApiKey)
  .handler(async ({ input, context }) => {
    if (context.apiKey) {
      // API key path
    } else if (context.user) {
      // Session path
    }
  });
```

See `references/api-keys.md` for the full API key route table, permission schema, and management examples.

## OpenAPI / REST

The host serves Scalar API reference docs and an OpenAPI spec:

| Endpoint | Content |
|----------|---------|
| `GET {hostUrl}/api` | Scalar HTML docs UI |
| `GET {hostUrl}/api` (Accept: application/json) | OpenAPI JSON spec |

Use the OpenAPI spec to generate typed clients in any language:

```bash
# Generate a TypeScript client
npx openapi-typescript-codegen --input https://dev.everything.dev/api --output ./generated-api

# Generate a Python client
openapi-generator-cli generate -i https://dev.everything.dev/api -g python -o ./api-client
```

REST routes follow the `oc.route({ method, path })` declarations in each plugin's contract. For example, `GET {hostUrl}/api/v1/registry/apps` maps to the `apps` plugin's `listRegistryApps` procedure.

### Plugin Manifests

Each deployed plugin exposes a manifest at `{pluginUrl}/plugin.manifest.json`:

```json
{
  "schemaVersion": 1,
  "kind": "every-plugin/manifest",
  "plugin": { "name": "apps", "version": "1.0.0" },
  "runtime": { "remoteEntry": "./remoteEntry.js" },
  "contract": {
    "kind": "orpc",
    "types": {
      "path": "./types/contract.d.ts",
      "exportName": "contract",
      "typeName": "ContractType",
      "sha256": "abc123..."
    }
  }
}
```

The manifest provides the contract type file path and SHA-256 checksum for verified type generation.

## Typed Clients via `bos types gen`

Child projects and the in-repo UI get typed API clients through generated contract bridge files.

### What It Generates

| File | Contents |
|------|----------|
| `ui/src/lib/api-types.gen.ts` | `ApiContract` — merged type of base API + all plugin contracts |
| `api/src/lib/plugins-types.gen.ts` | `PluginsClient` — factory types for in-process plugin composition |
| `*/src/lib/auth-types.gen.ts` | Auth plugin session/user/organization types |

### The Merged Contract

`api-types.gen.ts` intersects the base API contract with each plugin's contract as a namespaced key:

```ts
import type { ContractType as BaseApiContract } from "../../../api/src/contract.ts";
import type { ContractType as appsContract } from "../../../plugins/apps/src/contract.ts";

export type ApiContract = BaseApiContract & {
  auth: authContract;
  apps: appsContract;
  template: templateContract;
};
```

`ContractRouterClient<ApiContract>` turns this into a fully typed callable proxy — `apiClient.apps.listRegistryApps({ limit: 24 })` has complete input/output types.

### Regeneration Triggers

Generated files are gitignored and auto-regenerated on:
- `bun install` (via postinstall `bos types gen`)
- `bun typecheck`
- `bos dev` / `bos build`
- `bos plugin add` / `bos plugin remove`

After hand-editing `bos.config.json`, run `bos types gen` or restart `bos dev`.

### Type Resolution

- `local:plugins/<name>` → reads `src/contract.ts` directly from disk
- Remote URL → fetches `contract.d.ts` from the deployed plugin's manifest (with SHA-256 verification)
- Missing local path with no URL → skipped with a warning

## Child Project Config

A child project inherits the parent's host, API, auth, and plugins via the `extends` field in `bos.config.json`:

```json
{
  "extends": "bos://dev.everything.near/dev.everything.dev",
  "account": "myapp.near",
  "domain": "myapp.everything.dev"
}
```

This inherits:
- `app.host` — the parent's host server URL
- `app.ui` — the parent's UI CDN (overridable)
- `app.api` — the parent's API
- `app.auth` — the parent's auth plugin (via nested `extends` ref)
- `plugins` — all parent plugins (overridable per-key)

### Scaffolding with `bos init`

```bash
bos init myapp.everything.dev \
  --extends dev.everything.near/dev.everything.dev \
  --overrides ui,plugins \
  --plugins apps,template
```

This writes `bos.config.json` with `extends`, scaffolds the project, and generates types. The child's `postinstall` hook runs `bos types gen` automatically.

### What Can Be Overridden

Child configs can override `app.ui` (custom UI CDN) and `plugins.<key>` (custom plugin URLs). The host's `ALLOW_OVERRIDE` env var controls which sections tenants can customize (`ui`, `plugins`, `plugins.<key>`).

See the `extends-config` skill for deep merge semantics, per-environment extends, and canonical field ordering.

## Common Mistakes

- **Wrong header for API keys** — use `x-api-key`, not `Authorization: Bearer`. The `Authorization` header is only listed in CORS allowed headers for third-party integrations, not for everything.dev auth.
- **Missing `credentials: "include"`** — session cookies won't be sent without it. Both `createApiClient` and `createAuthClient` set this automatically; external clients must set it manually.
- **Not saving the API key secret** — `authClient.apiKey.create()` returns the full key string (`edk_...`) only once. Subsequent calls to `list` or `update` return only the prefix and metadata.
- **Plugin key vs plugin name confusion** — the client namespace is the `bos.config.json` `plugins` key (e.g., `"apps"`), not the Module Federation `name` field. They often match but are not guaranteed to.
- **Forgetting to regenerate types** — after adding/removing plugins in `bos.config.json`, run `bos types gen` or restart `bos dev`. Stale `api-types.gen.ts` will have missing or wrong namespace keys.
