Skip to main content

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.

  1. Input is validated against the port's declared inputSchema before the provider receives it.
  2. Output is validated against the port's declared outputSchema before the caller receives it.
  3. observedEffects must match the action's declared effects. An undeclared observed effect is a hard runtime failure.
  4. Activation fails if the loaded module's hash does not match the lockfile entry.

Packaging requirements

Providers must follow these packaging rules.

  1. Export providerModule as a named export from the package root.
  2. Use package.json exports for all public API. Do not use barrel files.
  3. Include both types and default entries in every exports path.
  4. 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:

  1. Snapshots the OpenAPI document into a versioned fixture.
  2. Applies the slice profile to select which operations to include.
  3. Applies the capability map to assign each operation to a Gooi port.
  4. 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