Use this checklist when adding, migrating, or changing a Chartbrew source.
Related docs:
- Keep source-specific runtime, UI, templates, and AI behavior owned by the source plugin.
- Prefer registry/capability checks over
connection.typeandconnection.subTypebranches. - Migrate one source at a time and remove old branches for that source in the same change.
- Keep backend availability separate from frontend creation availability.
- Reuse shared protocols only when behavior is genuinely shared.
- Do not add helper routes, compatibility thunks, or controller branches unless an active caller still needs them.
Each source has a registry identity plus persisted connection identity:
{
id: "stripe",
type: "api",
subType: "stripe",
}id: registry key for UI, templates, source lookup, and tests.type: persisted execution family, such asapi,postgres,mongodb, orcustomerio.subType: persisted brand/variant when needed.- Plain sources can use the same value for all three.
- Branded API sources should use
type: "api"and their brand assubType. - Variants should declare
dependsOn: ["<sourceId>"].
Use an optional availability block:
availability: {
server: { enabled: true },
ui: { canCreateConnections: true },
}- Server disabling uses
CB_DISABLED_SERVER_SOURCES. - UI creation disabling uses
VITE_DISABLED_UI_SOURCES. - Do not remove disabled sources from the registry; existing connections still need metadata, logos, builders, and clear errors.
- Enforce server disabling before hooks that can call a source: connection tests, previews,
runDataRequest, metadata/schema loading, actions, and AI tools. - UI disabling should only hide new-connection creation. Existing edit/build flows must still resolve the source.
Backend:
server/sources/plugins/<source>/<source>.plugin.js
server/sources/plugins/<source>/<source>.protocol.js
server/sources/plugins/<source>/<source>.connection.js
server/sources/plugins/<source>/ai/<source>.ai.js
server/sources/plugins/<source>/templates/
server/sources/shared/<shared-helper>.jsFrontend:
client/src/sources/<source>/<source>.source.js
client/src/sources/<source>/<source>-connection-form.jsx
client/src/sources/<source>/<source>-builder.jsx
client/src/sources/<source>/<source>-resource-query.jsx
client/src/sources/<source>/<source>-template-setup.jsx
client/src/sources/<source>/assets/Use source-prefixed filenames. Keep React component names PascalCase inside files.
Add server/sources/plugins/<sourceId>/<sourceId>.plugin.js:
const protocol = require("./<sourceId>.protocol");
module.exports = {
id: "<sourceId>",
dependsOn: [],
type: "<connectionType>",
subType: "<connectionSubType>",
name: "<Display name>",
category: "<category>",
description: "<short description>",
capabilities: {
connection: {
supportsTest: true,
supportsOAuth: false,
supportsFiles: false,
authModes: [],
},
data: {
supportsQuery: false,
supportsSchema: false,
supportsResourcePicker: false,
supportsPagination: false,
supportsVariables: true,
supportsJoins: true,
},
templates: {
datasets: false,
charts: false,
dashboards: false,
},
ai: {
canGenerateDatasets: false,
canGenerateQueries: false,
hasSourceInstructions: false,
hasTools: false,
},
},
backend: {
...protocol,
},
};Examples:
- Branded shared API:
server/sources/plugins/stripe/stripe.plugin.js - Custom protocol:
server/sources/plugins/customerio/customerio.plugin.js - Shared SQL wrapper:
server/sources/plugins/postgres/postgres.protocol.js
Update server/sources/index.js.
The registry provides:
getSourceById(id)getSourceForConnection(connection)findSourceForConnection(connection)getSourceSummaries()
Implement only what the source needs:
testConnection({ connection })testUnsavedConnection({ connection, extras })prepareConnectionData({ connection, extras })runDataRequest({ connection, dataRequest, chartId, getCache, filters, timezone, variables, auditContext })previewDataRequest({ connection, dataRequest, itemsLimit, items, offset, pagination, paginationField })getBuilderMetadata({ connection, dataRequest, options })getSchema({ connection, dataRequest })applyVariables({ dataRequest, variables })actions
Rules:
- Keep custom runtime behavior in source-owned files, not controllers.
prepareConnectionData(...)may enrich connection payloads before save; catch best-effort failures and return the original connection.- Variable substitution is source-owned through
backend.applyVariables(...); the dispatcher isserver/sources/applySourceVariables.js. - Branded API sources can reuse
server/sources/shared/protocols/api.protocol.js. - SQL variants can reuse
server/sources/shared/sql/sql.protocol.js, but still keep source-owned wrappers for source identity, defaults, AI, templates, and variants.
Expose source-specific helper calls as plugin actions:
const actions = {
getAllSegments({ connection }) {
return sourceImplementation.getAllSegments(connection);
},
};
module.exports = {
capabilities: {
actions: Object.keys(actions),
},
backend: {
...protocol,
actions,
},
};Actions are called through:
POST /team/:team_id/connections/:connection_id/source-actionDo not add new /helper/:method routes.
Runtime execution should resolve the source from:
server/sources/runSourceDataRequest.jsCallers:
server/controllers/DataRequestController.jsserver/controllers/DatasetController.js
When migrating a source, route it through source.backend.runDataRequest(...) and remove old fallback branches.
Source-aware connection routes live in:
server/api/ConnectionRoute.jsThese paths should use plugin methods:
GET /team/:team_id/connections/:connection_id/testPOST /team/:team_id/connections/:type/testPOST /team/:team_id/connections/:type/test/filesPOST /team/:team_id/connections/:connection_id/apiTestPOST /team/:team_id/connections/:connection_id/source-action
Update or add:
server/tests/unit/sourceRegistry.test.js
server/tests/integration/connectionRoute.security.test.jsMinimum checks:
- resolves by
id - resolves from persisted connection shape
- exposes expected backend methods
- runtime dispatcher finds the migrated source
- unlisted actions are rejected
- project-scoped users cannot access restricted source actions
- preview/test routes call plugin hooks
Create:
client/src/sources/<source>/<source>.source.js
client/src/sources/<source>/assets/Keep *.source.js free of React imports.
Typical shape:
{
id: "<sourceId>",
type: "<connectionType>",
subType: "<connectionSubType>",
name: "<Display name>",
category: "<category>",
availability: {
ui: { canCreateConnections: true },
},
capabilities: {
ai: { canGenerateQueries: false },
templates: { charts: false },
nextSteps: { chartTemplates: false },
},
assets: {
lightLogo,
darkLogo,
},
}Update client/src/sources/index.js:
import ConnectionForm from "./<source>/<source>-connection-form";
import DataRequestBuilder from "./<source>/<source>-builder";
import ChartTemplateSetup from "./<source>/<source>-template-setup";
const FRONTEND_BY_SOURCE_ID = {
<sourceId>: {
ConnectionForm,
DataRequestBuilder,
ChartTemplateSetup,
},
};Rules:
- Do not add source-specific form/builder branches to shared screens.
- Keep custom template setup UI under
client/src/sources/<source>/. - UI-disabled sources should be hidden only from creation; existing edit/builder flows still resolve by registry.
Use runSourceAction(...) from client/src/slices/connection.js.
Do not add runHelperMethod thunks or helper routes.
- Connection display:
client/src/modules/getConnectionLogo.js - Source picker cards:
getSourceLogo(source, isDark)
If the source ships built-in chart templates:
-
Put files under
server/sources/plugins/<sourceId>/templates/. -
Add backend template metadata:
const path = require("path"); templates: { directory: path.join(__dirname, "templates"), chartTemplates: ["template-id"], defaults: { dataRequest: DEFAULT_DATA_REQUEST, }, }
-
Add frontend template metadata:
capabilities: { templates: { charts: true }, nextSteps: { chartTemplates: true }, }, templates: { chartTemplates: ["template-id"], }, defaults: { dataRequest: {}, },
-
Custom setup UI goes in
client/src/sources/<sourceId>/<sourceId>-template-setup.jsx. -
Built-in templates must resolve through registered source plugins.
-
Template setup UIs should expose one selectable card per chart, grouped by domain when useful. Selecting one chart must create only that chart's required datasets, not the entire template bundle.
Template chart bindings:
{
id: "revenue-vs-fees",
requiredDatasetIds: ["gross_revenue", "fees"],
cdcs: [{
datasetTemplateId: "gross_revenue",
xAxis: "root[].period",
yAxis: "root[].value",
legend: "Gross revenue",
}],
}Use layoutIntent instead of hard-coded grid coordinates:
{
id: "net-revenue-kpi",
layoutIntent: {
kind: "kpi",
priority: 10,
},
}Supported kinds: kpi, trend, comparison, table.
Pick exactly one runtime AI mode.
Use for SQL-like or Mongo-like sources where Chartbrew generates a read-only query over a schema.
Capabilities:
ai: {
canGenerateDatasets: true,
canGenerateQueries: true,
hasSourceInstructions: true,
hasTools: false,
}Backend wiring:
backend: {
...protocol,
ai: {
getCapabilities: () => getQueryGenerationCapabilities("<sourceId>"),
getSchema: protocol.getSchema,
generateQuery: protocol.generateQuery,
instructions: getQueryGenerationInstructions("<sourceId>"),
},
}Rules:
- Put compact dialect hints in
server/sources/shared/ai/queryGenerationInstructions.js. - Keep hints short: read-only, dialect syntax, date bucketing, limits, variables, caveats.
get_schemareturnssourceInstructions.generate_queryinjectssourceInstructionsinto the schema sent tobackend.ai.generateQuery(...).- Do not expose source-owned
planDataset.
Use for sources where Chartbrew plans configuration, routes, paths, or API request options instead of free-form queries.
Capabilities:
ai: {
canGenerateDatasets: true,
canGenerateQueries: false,
hasSourceInstructions: true,
hasTools: true,
}Backend wiring:
const sourceAi = require("./ai/<sourceId>.ai");
backend: {
...protocol,
ai: sourceAi,
}AI module path:
server/sources/plugins/<sourceId>/ai/<sourceId>.ai.jsImplement only needed methods:
getCapabilities({ connection })listResources({ connection })getSchema({ connection })getSampleData({ connection, resource, rowLimit })planDataset({ connection, question, overrides })validateConfiguration(configuration, { connection })previewConfiguration({ connection, configuration, rowLimit })listTemplates({ connection })recommendTemplates({ connection, question })
Rules:
- Use generic orchestrator tools from
server/modules/ai/orchestrator/tools/sourceTools.js. - Do not add per-source top-level orchestrator tools.
- Do not expose
generateQuery. - Keep outputs compact, capped, and secret-free.
- Do not return raw docs, auth headers, bearer tokens, OAuth tokens, or large schemas.
- Generic API sources can use free-form AI Context; only recognizable provider hosts may allow model/provider fallback.
Planner statuses:
ok: include source DataRequest fields andchartSpecneeds_more_context: includemessage,requiredContext, and optional edit/context guidanceneeds_disambiguation: include compactoptionsneeds_model_planning: only for generic API/provider fallbackunsupportedorerror: include actionablemessageorerrors
AI-created charts must persist safe ChartDatasetConfigs:
- Table: planner may omit
xAxisonly if creation defaults it toroot[]. - KPI/avg/gauge: require
yAxis; missingxAxismust fall back toyAxis. - Line/bar/pie/doughnut/radar/polar: require both
xAxisandyAxis. - Timeseries: provide date-compatible
xAxisordateFieldwhen date filtering is expected.
Normalize or reject unsafe chart payloads before rendering.
When AI behavior changes, update:
server/tests/unit/sourceAiHarness.test.jsThe harness is deterministic and must not call the LLM. It should check:
- planner statuses, DataRequest shape, and chart specs
- query-generation instructions through
get_schemaandgenerate_query - source-owned sources do not expose
generateQuery - query-generation sources do not expose
planDataset - generic
source_*outputs are compact, capped, and secret-free - temporary and saved charts persist safe CDC bindings
- high-risk tool sequences stay in the intended mode
Keep fixtures small and invariant-focused. Add source-specific examples only for domain rules generic assertions cannot express.
Search for old source branches:
rg "<sourceId>|<Connection.type>|<Connection.subType>" server/controllers server/api client/srcRemove migrated-source branches from:
ConnectionControllerDataRequestControllerDatasetController- route-specific helper endpoints
- frontend builder/form switch statements
- one-off logo maps
Generic protocol files may still mention protocol names.
Run the relevant focused checks:
cd server && npm run test:run -- tests/unit/sourceRegistry.test.js tests/integration/connectionRoute.security.test.js
cd server && npm run test:run -- tests/unit/sourceAiHarness.test.js
cd server && npm run lint
cd client && npm run lint
cd client && npm run buildAdd source-specific focused tests when templates, protocol behavior, schema loading, frontend flows, or AI behavior are touched.