| 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:
{
hostUrl: "https://dev.everything.dev",
apiBase: "/api",
rpcBase: "/api/rpc",
authAvailable: true,
account: "dev.everything.near",
networkId: "mainnet",
}
Read it with getRuntimeConfig() from everything-dev/ui/runtime:
import { getRuntimeConfig } from "everything-dev/ui/runtime";
const config = getRuntimeConfig();
Or use the @/app helpers (in-repo UI only):
import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app";
const account = getAccount();
const runtime = getActiveRuntime();
const appName = getAppName();
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:
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:
{
"plugins": {
"apps": { "production": "https://..." },
"template": { "production": "https://..." }
}
}
Then the client surface is:
client.ping();
client.apps.listRegistryApps();
client.template.someProcedure();
client.auth.getSession();
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:
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):
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:
import { createApiClient, useApiClient, useOrpc } from "@/app";
const apiClient = createApiClient({
hostUrl: runtimeConfig.hostUrl,
rpcBase: runtimeConfig.rpcBase,
});
const apiClient = useApiClient();
const result = await apiClient.apps.listRegistryApps({ limit: 24 });
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:
import { createAuthClient, sessionQueryOptions, useAuthClient } from "@/app";
const authClient = createAuthClient({ runtimeConfig, cspNonce });
const authClient = useAuthClient();
SIWN Configuration
The auth client reads NEAR recipient config from runtimeConfig.auth.variables.siwn:
{
recipient: "dev.everything.near",
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
await authClient.signIn.siwn({ networkId: "mainnet" });
await authClient.signIn.email({ email, password });
await authClient.signIn.passkey();
await authClient.signOut();
await authClient.organization.setActive({ organizationId: "org_123" });
Session Query
import { sessionQueryOptions } from "@/app";
const session = await queryClient.ensureQueryData(
sessionQueryOptions(authClient, context.session),
);
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:
const authClient = createAuthClient({
runtimeConfig,
headers: request.headers,
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.*):
const authClient = useAuthClient();
const { data, error } = await authClient.apiKey.create({
name: "my-bot-key",
configId: "org-keys",
organizationId: "org_123",
expiresIn: 60 * 60 * 24 * 30,
});
if (data) {
console.log(data.apiKey.key);
}
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:
const link = new RPCLink({
url: `${HOST}/api/rpc`,
fetch: globalThis.fetch,
headers: { "x-api-key": "edk_xxxxxxxxxxxxxxx" },
});
Or with plain fetch to REST endpoints:
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:
builder.deleteThing
.use(requireApiKey({ things: ["delete"] }))
.handler(async ({ context }) => {
context.apiKey.permissions;
});
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:
builder.createThing
.use(requireAuthOrApiKey)
.handler(async ({ input, context }) => {
if (context.apiKey) {
} else if (context.user) {
}
});
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:
npx openapi-typescript-codegen --input https://dev.everything.dev/api --output ./generated-api
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:
{
"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:
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:
{
"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
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.