Skip to main content
Prefer depth over breadth. A provider that handles the real flows of its official SDK beats one that answers CRUD. Before starting, open a provider request so the scope is agreed.

Scaffold

1

Generate the package

The name must be lowercase letters and digits. Pass --label "Acme API" to pnpm new:provider to set the display label (it defaults to the capitalized name).
2

Install and build

Commit the updated pnpm-lock.yaml.
3

Copy the checklist

The script prints the provider definition of done. Paste it into your pull request body.
To undo the most recent scaffold, run pnpm new:provider acme --remove. It only removes the last provider in the registry, and refuses providers that shipped in 0.14.0, because removing any other slot would shift later ports.

What the scaffold creates

packages/@deploybaseline/acme/ with:
  • package.json, tsconfig.json, tsup.config.ts (entries src/index.ts, src/manifest.ts, and src/provider.ts), vitest.config.ts using providerTestConfig
  • README.md, LICENSE, THIRD_PARTY_NOTICES.md
  • src/manifest.ts, src/entities.ts, src/store.ts, src/index.ts
  • src/provider.ts, the process entry the CLI forks: one call to core’s runProvider(manifest)
  • src/routes/items.ts (sample routes) and src/routes/inspector.ts
  • src/__tests__/coverage.ts (matrix), coverage.test.ts (gate 1), coverage.setup.ts (gate 2, in fail mode)
  • src/__tests__/acme.sdk.test.ts, a test skeleton including a restartService check and an inspector check

Provider definition of done

This is the checklist scripts/new-provider.mjs prints:
  • Routes match the vendor’s API: paths, shapes, status codes, and error bodies
  • Tests drive the emulator with the official SDK, pinned to an exact version
  • Coverage matrix lists every route; the gate passes in fail mode
  • All state lives in the store; a restartService test shows reads match after a restart
  • Seed config documented; initConfig in src/manifest.ts is a useful starter
  • Inspector page uses the shared UI in core ui.ts
  • Docs: README row, docs/acme.mdx and its docs.json entry, skills/acme/SKILL.md, CLI --help if it adds flags
  • Vendored specs or schemas have a license check and a THIRD_PARTY_NOTICES.md entry
  • npm first publish bootstrap for @deploybaseline/acme (a new package name needs a placeholder publish and a trusted publisher, see Bootstrapping package names)
The last item is a maintainer task. The release workflow refuses to run while any workspace package is missing from npm. See Releasing.

Anatomy of a finished provider

@deploybaseline/resend is a compact, complete example:

State rules

  • Keep every piece of state in the store: collections, or store.setData for maps and flags. Never in module variables or closures inside register.
  • Namespace collection names with the provider (resend.emails).
  • Every provider has a state-restart test: run a main flow, call restartService from @deploybaseline/testing (save, then start a fresh instance from the saved JSON on the same port), and check that reads match and tokens still work.
  • Bump stateVersion in src/manifest.ts when an entity shape changes in a way older saved state cannot load.
  • Changes to how a runtime saves or loads state must keep the shared contract in tests/contracts/persistence.js passing for createEmulator and the start runner.

Webhooks and UI

  • Sign deliveries with the core helpers (svixSignature, hmacSha256Hex, and the rest), or a provider’s own signing.ts for a scheme no other provider uses. See Architecture.
  • Build inspector, OAuth, and checkout pages with the render functions in core/src/ui.ts. See Shared UI.

GraphQL providers

  • Serve the endpoint with graphqlEndpoint and build the schema with buildSchema from @deploybaseline/graphql.
  • Start from the vendor’s published SDL. node scripts/vendor-graphql-schema.mjs <name> fetches it and writes src/graphql/schema.generated.ts (descriptions trimmed for size). Add the source to that script with its license, and add a THIRD_PARTY_NOTICES.md entry.
  • The coverage matrix lists every root field in the schema (Query.issues, Mutation.issueCreate) next to the routes. A root field without a resolver stays not_started and answers “not emulated”.

Composed providers

For several APIs behind one port and seed key, build the plugin with composeService (see Composed services):
  • Each module registers its routes and declares the namespaced collections it owns.
  • Put each module’s matrix under its name in src/__tests__/coverage.ts: export const modules = { gmail: [...], drive: [...] }.
  • Check it with checkModuleCoverage and pass modules to coverageGate. A route listed under the wrong module fails, the CI job summary gets one row per module, and pnpm conformance publishes per-API counts.
  • node scripts/generate-coverage-matrix.mjs <name> writes the module layout.

Before opening the pull request