Subgraphs and attachments
A Switchboard serves more than the reactor's document API. Your project's own subgraphs sit next to it, and attachments come from a service the app supplies. The light client reaches both through the configuration it already has: one URL, one token provider.
Typed subgraphs
A subgraph is served at <graphql base>/<name>. For a Switchboard on
http://localhost:4001/graphql, the renown-read-model subgraph is at
http://localhost:4001/graphql/renown-read-model.
client.subgraph(name, getSdk) derives that URL from the client's url, keeps
one transport per name, and hands the SDK the same auth middleware every reactor
call goes through.
"use client";
import { useSwitchboardClient } from "@powerhousedao/reactor-browser/graphql-client";
import { getRenownReadModelSdk } from "@/lib/renown-read-model-sdk";
const UNKNOWN_ADDRESS = "0x0000000000000000000000000000000000000000";
export function Probe() {
const client = useSwitchboardClient();
async function probe() {
if (!client) return;
const sdk = client.subgraph("renown-read-model", getRenownReadModelSdk);
const { renownUsers } = await sdk.RenownUsers([UNKNOWN_ADDRESS]);
return renownUsers;
}
return (
<button disabled={!client} onClick={() => void probe()}>
Probe subgraph
</button>
);
}
subgraph gives you transport, auth and typing. Results are not cached, produce
no change events and never reach useDocument. Cache them with whatever the app
already uses for server state.
The registry caches the transport per name, but reuses the SDK only when the same factory function is passed again. Two callers with two different generated SDKs each get their own, rather than the first caller's methods under the second caller's type.
The root supergraph federates every subgraph, so a field a subgraph defines is
usually reachable at the base URL too. If you need to prove a request went to the
subgraph's own endpoint, query _service { sdl }: the root has no _service
field.
Generate the SDK
Point graphql-codegen at the subgraph's endpoint and use the same three plugins
@powerhousedao/reactor-browser generates its own SDK with:
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://localhost:4001/graphql/renown-read-model",
documents: ["src/**/*.graphql"],
generates: {
"./src/graphql/renown-read-model.ts": {
plugins: [
"typescript",
"typescript-operations",
"typescript-graphql-request",
],
config: {
scalars: {
JSONObject: "NonNullable<unknown>",
DateTime: "string | Date",
},
skipTypename: true,
maybeValue: "T | null | undefined",
gqlImport: "graphql-tag#gql",
},
},
},
};
export default config;
The typescript-graphql-request plugin emits
getSdk(client: GraphQLClient, withWrapper?: SdkFunctionWrapper), which is
exactly the SubgraphSdkFactory<TSdk> signature subgraph expects. Pass the
generated function straight in.
export type SubgraphSdkFactory<TSdk> = (
client: GraphQLClient,
withWrapper?: SdkFunctionWrapper,
) => TSdk;
SdkFunctionWrapper is re-exported from @powerhousedao/reactor-browser, so a
hand-written SDK can name the type without depending on graphql-request
directly. An SDK the client builds is always called with the auth wrapper: apply
it to every operation, exactly as the generated code does.
One-off documents
For an operation not worth generating an SDK for, use client.request. It runs
against the Switchboard's own endpoint, through the same transport and auth.
const DOCUMENT_MODEL_IDS = `
query DocumentModelIds {
documentModels {
items {
id
}
}
}
`;
type DocumentModelIds = {
documentModels: { items: { id: string }[] };
};
const { documentModels } =
await client.request<DocumentModelIds>(DOCUMENT_MODEL_IDS);
A document a code generator typed carries its own result type, so the type
argument is only needed for hand-written documents and strings. The operation
name and type are read off the document; override them through the third
argument, which also takes an AbortSignal.
Attachments
Attachments are configured at the same point as everything else, but the app
constructs the service. Which storage it talks to is the app's business, and
@powerhousedao/reactor-browser does not depend on the attachment
implementation.
<GraphQLReactorProvider
url={switchboardUrl}
attachmentService={attachmentService}
>
<YourApp />
</GraphQLReactorProvider>
The provider publishes it into the window.ph.attachmentService slot Connect
publishes it into, so useAttachmentService and the editors below find it. Leave
the prop out and the slot stays empty, exactly as it is without this provider.
This prop is a wiring seam, and that is all it has been proven to be: the service
an app passes in reaches useAttachmentService below the provider. No attachment
has been read over the light client end to end.
It is published from its own effect, keyed on the prop, so an app that builds the service asynchronously can pass it in later. Passing a new service does not rebuild the client or the document cache.
Build the service with createRemoteAttachmentService from
@powerhousedao/reactor-attachments/client, pointed at the same Switchboard.
Import the /client subpath, not the package root: the root entry also exports
the Kysely, S3 and filesystem backends, so a browser bundler has to resolve
kysely, @aws-sdk/client-s3 and node:fs and fails before any app code runs -
the same failure class the light client's own
Bundling
section describes, and it has the same shape of fix: import the browser subpath. See
Attachment service for the
service and client APIs, the reserve-send-read flow, and the attachment://v1:
ref format.