Wiring and Packaging
With all the pieces built, it’s time to wire them into an installable connector and set up the package structure.
ConnectorDef
Ties schema, resolvers, operations, seeder, and onboarding into a single descriptor:
import { ConnectorDef, ConnectorModule, Installation } from "@max/connector";import { Context } from "@max/core";import { AcmeOperations } from "./operations.js";
const AcmeDef = ConnectorDef.create<AcmeConfig>({ name: "acme", displayName: "Acme", description: "Project management connector powered by Acme", icon: "", version: "0.1.0", scopes: [], schema: AcmeSchema, onboarding: AcmeOnboarding, seeder: AcmeSeeder, resolvers: [ AcmeRootResolver, AcmeUserResolver, AcmeWorkspaceResolver, AcmeProjectResolver, ], operations: [...AcmeOperations],});ConnectorModule
Pairs the def with an initialise function that creates a live Installation:
const AcmeConnector = ConnectorModule.create<AcmeConfig>({ def: AcmeDef, initialise(config, credentials) { const tokenHandle = credentials.get(AcmeApiToken); const api = new AcmeConnection(config, tokenHandle);
const ctx = Context.build(AcmeAppContext, { api, workspaceId: config.workspaceId, });
return Installation.create({ context: ctx, async start() { await api.start(); credentials.startRefreshSchedulers(); }, async stop() { credentials.stopRefreshSchedulers(); }, async health() { const result = await api.health(); return result.ok ? { status: "healthy" } : { status: "unhealthy", reason: result.error ?? "Unknown error" }; }, }); },});
export default AcmeConnector;The default export is the ConnectorModule. This is what the registry imports.
What happens during initialise
credentials.get(AcmeApiToken)returns aCredentialHandle- a lazy handle, not the raw secret- You build your API client wrapper, passing the handle (credentials aren’t resolved yet)
Context.build()hydrates the context class with real valuesInstallation.create()packages context + lifecycle hooks- Later, the platform calls
start()- that’s when credentials resolve and the HTTP client is constructed
Installation lifecycle
| Hook | When | Purpose |
|---|---|---|
start() | Before first sync | Resolve credentials, create HTTP clients |
stop() | On shutdown | Clean up schedulers, close connections |
health() | On demand | Lightweight connectivity check |
Package setup
Where connectors live
Connectors live in a connector collection - a directory (or repo) containing one or more connector-* folders. A collection can live anywhere; it doesn’t need to be inside the Max monorepo.
Install a collection into Max with:
# Local pathmax -g install --collection /path/to/my-connectors
# Git URLmax -g install --collection git@github.com:my-org/max-connectors.gitThe registry scans installed collections for connector-* folders, reads each package.json, and registers a lazy loader. Your connector is only imported when first needed.
Collection layout
A collection is a Bun workspace with one or more connectors:
my-connectors/├── package.json├── connector-github/│ ├── package.json│ └── src/│ └── index.ts├── connector-linear/│ ├── package.json│ └── src/│ └── index.tsCollection root package.json
{ "name": "my-connectors", "private": true, "workspaces": { "packages": ["connector-*"], "catalog": { "@max/core": "link:@max/core", "@max/connector": "link:@max/connector", "@types/bun": "latest", "typescript": "5.9.3" } }}The link: entries resolve to your local Max checkout via bun link. Running bun install in the Max monorepo links @max/core and @max/connector as global packages. Running bun install in your collection picks them up.
Connector package.json
{ "name": "@max/connector-acme", "version": "0.1.0", "type": "module", "exports": { "types": "./src/index.ts", "default": "./src/index.ts" }, "scripts": { "build": "bun build ./src/index.ts --outdir ./dist --target node", "typecheck": "tsc --noEmit", "test": "bun test --pass-with-no-tests" }, "dependencies": { "@max/core": "catalog:", "@max/connector": "catalog:" }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:" }}Use "catalog:" for @max/core and @max/connector - this resolves through the collection root’s catalog. Point both types and default exports to source.
File structure
Here’s the complete connector layout:
connectors/connector-acme/├── package.json├── tsconfig.json├── src/│ ├── index.ts # ConnectorDef + ConnectorModule (default export)│ ├── config.ts # TConfig interface│ ├── entities.ts # Entity definitions│ ├── schema.ts # Schema│ ├── credentials.ts # Credential declarations│ ├── context.ts # Context definition│ ├── onboarding.ts # OnboardingFlow│ ├── seeder.ts # Seeder + SyncPlan│ ├── acme-client.ts # API client wrapper│ └── resolvers/│ ├── root-resolver.ts│ ├── user-resolver.ts│ ├── workspace-resolver.ts│ └── project-resolver.tsYour connector is complete
You now have a working, installable connector with:
- A typed data model (entities, schema, context)
- A sync pipeline (loaders, resolvers, seeder)
- Operations wrapping every API call
- An onboarding flow for user setup
- Assembly (ConnectorDef, ConnectorModule, Installation)
- A publishable package structure
For most connectors, this is everything you need. The next part covers an advanced optimization for connectors that need to extract multiple entity types from a single API endpoint.
Next: Advanced Patterns