Building a Provider
A provider is a package that implements one or more capability ports. When your spec calls collections.write or ids.generate, a provider is the runtime implementation behind that call.
Why this exists
Without a provider SDK, building a provider requires reverse-engineering the capability port contract from the runtime source. There is no standard manifest format, no typed boundary, and no way to verify your implementation before deploying it.
The provider SDK gives you a typed framework for implementing capability ports, declaring your manifest, and verifying your implementation against the conformance suite before publication.
Installation
bun add @gooi/provider-sdk
# or
npm install @gooi/provider-sdk
Minimal provider
A provider exports a providerModule that implements the ProviderModule contract.
import type { ProviderModule } from "@gooi/provider-runtime";
export const providerModule: ProviderModule = {
manifest: {
providerId: "my-org.my-provider",
providerVersion: "1.0.0",
hostApiRange: "^1.0.0",
capabilities: [
{
portId: "ids.generate",
portVersion: "1.0.0",
contractHash: "<sha256 of the port contract>",
},
],
},
activate: async () => ({
invoke: async ({ port, input }) => {
if (port === "ids.generate") {
const ids = Array.from({ length: input.count }, (_, i) =>
`${input.prefix ?? ""}${Date.now()}_${i}`
);
return { ok: true, output: { ids }, observedEffects: ["compute"] };
}
return { ok: false, error: { code: "UNKNOWN_PORT", port } };
},
deactivate: async () => undefined,
}),
};
Provider lifecycle
A provider has three lifecycle methods.
activate(): Called once at runtime startup. Returns an object with invoke and deactivate. Use this to open connections, load credentials, and prepare any shared state.
invoke({ port, input, traceId }): Called for every capability invocation. Receives the port name and validated input. Returns { ok: true, output, observedEffects } on success or { ok: false, error } on failure.
deactivate(): Called at runtime shutdown. Use this to close connections and flush pending state.
Using the provider SDK
The provider SDK simplifies common provider patterns.
import { createConnector, staticApiKeyAuth, fieldCodec } from "@gooi/provider-sdk";
export const myDataProvider = createConnector({
id: "@my-org/my-data-provider",
version: "1.0.0",
auth: staticApiKeyAuth({ header: "X-Api-Key" }),
ports: {
"collections.read": {
async invoke({ collection, where, sort, page }, ctx) {
const response = await ctx.http.get(`/v1/${collection}`, {
params: { ...where, ...sort, ...page },
});
return {
rows: response.data.items.map((item) => fieldCodec.decode(item)),
};
},
},
"collections.write": {
async invoke({ patch }, ctx) {
await ctx.http.post(`/v1/${patch.collection}`, patch.value);
return { ok: true };
},
},
},
});
The provider SDK provides:
- Auth strategy modules: OAuth2, static API key, bearer token
- A typed HTTP client pre-wired with retry policy and timeout enforcement
- Field codecs for converting between Gooi field types and provider-native types
- Pagination adapters for normalizing cursor-based and offset-based pagination
Runtime enforcement
The kernel enforces the capability boundary on every invocation.
- Input is validated against the port's declared
inputSchemabefore the provider receives it. - Output is validated against the port's declared
outputSchemabefore the caller receives it. observedEffectsmust match the action's declaredeffects. An undeclared observed effect is a hard runtime failure.- Activation fails if the loaded module's hash does not match the lockfile entry.
Packaging requirements
Providers must follow these packaging rules.
- Export
providerModuleas a named export from the package root. - Use
package.jsonexportsfor all public API. Do not use barrel files. - Include both
typesanddefaultentries in everyexportspath. - Keep provider modules functional. Avoid class hierarchies.
{
"name": "@my-org/my-provider",
"version": "1.0.0",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
Building from an OpenAPI spec
If you have an existing OpenAPI spec, @gooi/provider-sdk-openapi generates the port implementations automatically.
bun add @gooi/provider-sdk-openapi
bun gooi-openapi ingest \
--spec ./openapi.json \
--slice-profile ./slice-profile.json \
--capability-map ./capability-map.json \
--out ./src/generated
The ingestion process:
- Snapshots the OpenAPI document into a versioned fixture.
- Applies the slice profile to select which operations to include.
- Applies the capability map to assign each operation to a Gooi port.
- Generates typed port implementations and a provider manifest.
Validate the generated descriptors against canonical contract schemas before shipping.
Before you publish
Run the conformance suite against your provider before publication.
import { runProviderConformance } from "@gooi/conformance/provider";
const results = await runProviderConformance({
providerModule,
requiredPorts: ["collections.write", "collections.read"],
});
for (const result of results) {
console.log(result.checkId, result.passed ? "PASS" : "FAIL");
}
A provider that passes conformance is ready for publication. Refer to the provider publication checklist in docs/engineering/providers/provider-publication-checklist.md for the full set of required evidence before submitting for marketplace certification.
What's next
- Capability Binding: how the resolver selects your provider and produces a lockfile
- Control Plane: how provider-owned adapters participate in report/plan/apply/drift/generate workflows
- The Marketplace: how to submit your provider for certification and trust-tier elevation