Skip to main content

Integrations architecture

Project Brain connects to external systems through a provider registry. Jira, Confluence, and GitHub Issues all use the same dispatch model, so most app routes and UI pages do not need provider-specific code.

Core idea

  • Providers are registered by id, such as jira, confluence, or github_issues.
  • Connect, callback, preview, save, queue, worker sync, scope pages, and provider grids all dispatch through the registry.
  • Server-side registry and provider wiring live in @projectbrain/integrations.
  • Provider SDK clients and auth live in their own packages — @projectbrain/atlassian (OAuth + HTTP client shared by Jira and Confluence) and @projectbrain/github (the GitHub App: manifest registration, config, installation tokens) — which the server providers wrap.
  • Browser-safe scope form code lives in @projectbrain/integrations/ui.

This split keeps database clients, provider SDKs, and HTTP clients out of the browser bundle.

Package layout

packages/
adapters-jira/
adapters-confluence/
adapters-github-issues/
atlassian/ # shared Atlassian OAuth + HTTP client + config
src/
oauth.ts
atlassian-client.ts
resources.ts
config.ts
github/ # GitHub App: manifest, config, installations, JWT signing
src/
manifest.ts
config.ts
installations.ts
app-jwt.ts
integrations/
src/
types.ts
registry.ts
tokens.ts
sync/orphans.ts
oauth/atlassian.ts
providers/
jira.ts
confluence.ts
github-issues.ts
index.ts # module-load registration (registerProvider calls)
ui/
types.ts
jira.ts
confluence.ts
github-issues.ts
index.ts

apps/
web/components/scope-form.tsx
web/app/(app)/projects/[slug]/integrations/
web/app/api/integrations/
worker/src/workers/sync.ts

Provider contract

Each provider has two halves joined by the same id.

Server provider

Runs in Next.js API routes and the worker.

interface IntegrationProvider<TScope, TSample> {
id: string;
name: string;
blurb: string;
icon: string;
docs?: ProviderDocs;
isConfigured(): Promise<boolean>;
connect: ConnectFlow;
getAccessToken(integration): Promise<string>;
scope: {
schema: ZodType<TScope>;
empty: TScope;
};
preview(args): Promise<PreviewResult<TSample>>;
sync: {
kind;
pathPrefix;
pathRegex;
run;
};
}

UI provider

Runs in the browser scope form.

interface ScopeUiConfig<TScope> {
fields: ScopeFieldConfig[];
queryLabel: string;
itemNoun: string;
formToScope(form): TScope;
scopeToForm(scope): Record<string, string>;
summarizeSample(item): SampleItemSummary;
}

Connect flows

ConnectFlow is a discriminated union.

KindUse caseExample
oauth-redirectProvider consent flow with callback and token storageAtlassian
project-resourceAdopt an existing project resource without OAuth token storageGitHub App installation

The connect route switches on kind. The OAuth callback route only handles oauth-redirect.

GitHub registration uses a separate manifest flow (/api/admin/integrations/github/manifest → GitHub manifest endpoint → /api/github/manifest/callback) that creates the App on GitHub and persists the resulting App id, slug, client id/secret, webhook secret, and PEM to platform_integrations (encrypted). This sits in front of the project-resource connect, which then attaches an installation id to the same row. See packages/github/src/manifest.ts and packages/github/src/config.ts.

Add a provider

  1. Create an adapter package under packages/adapters-X/.

  2. Add scope schema, client, fetch, preview, iteration, and transform code.

  3. Add an OAuth family under packages/integrations/src/oauth/ if the provider uses OAuth.

  4. Add the server provider in packages/integrations/src/providers/X.ts.

  5. Register the server provider in packages/integrations/src/providers/index.ts.

  6. Add browser-safe UI config in packages/integrations/src/ui/X.ts.

  7. Register the UI config in packages/integrations/src/ui/index.ts.

  8. Extend integration_type in packages/db/src/schema/integrations.ts.

  9. Run:

    pnpm --filter @projectbrain/db generate
  10. Update casts that mention existing provider ids:

    rg "as 'jira' \\| 'confluence'"
  11. Add an icon key in apps/web/components/icons.tsx if needed.

After registration, the provider grid, dynamic scope page, and worker sync dispatcher pick it up automatically.

Important design choices

Server and client registries are separate

The server registry imports database and provider code. The UI registry imports only browser-safe field definitions and pure mapping functions.

Client components import:

@projectbrain/integrations/ui

They should not import:

@projectbrain/integrations

Functions do not cross the RSC boundary

React Server Components can only pass serializable props. The server page passes providerId, and the client scope form calls getProviderUi(providerId).

Hooks stay inside the inner form

The outer scope form handles missing provider UI and returns an error state if needed. The inner form receives a valid ui object and runs hooks unconditionally.

Token lifecycle belongs to the provider

OAuth providers call the shared getOAuthAccessToken(integration, family) helper. GitHub Apps mint installation tokens on demand through Octokit instead of using refresh tokens — the App's own credentials (id, client id/secret, webhook secret, PEM) live encrypted on platform_integrations, loaded via loadGitHubAppConfig() with a short-lived in-memory cache.

Orphan detection is shared

detectAndStageDeletions is shared in:

packages/integrations/src/sync/orphans.ts

Each provider declares sync.pathPrefix and sync.pathRegex. The helper compares seen external ids against existing files and stages deletions.

Current limitations

LimitationImpact
integration_type is a Postgres enumAdding a provider requires a Drizzle migration.
Some provider id casts are manualUpdate casts when adding providers.
Multi-workspace selection auto-picks the first workspaceUsers with multiple Atlassian sites or GitHub installations cannot choose yet.
Scope form layout is genericComplex provider UIs may need a future custom field escape hatch.
Preview sample items are typed as unknown in the registryEach UI config casts locally in summarizeSample.
Single GitHub installation per deploymentSwitching org replaces the existing installation; you can't sync from multiple GitHub orgs concurrently.

Connecting an IDE via MCP

The sections above cover how Project Brain ingests external systems. The reverse direction — exposing a project's knowledge base to an IDE — is handled by an MCP server, configured in-app.

A project admin opens a project, clicks the gear menu in the project shell, and chooses Connect to IDE (MCP) (the menu is admin-only). The modal walks through three steps:

  1. Install the mcp-project-brain binary with the one-liner for your OS (PowerShell on Windows, curl … | bash on macOS/Linux).
  2. Set a GitHub token — a fine-grained PAT scoped to the project's knowledge-base repo with Contents: Read and write, exported as GITHUB_PERSONAL_ACCESS_TOKEN.
  3. Add the MCP server to NeusisCode — copy the generated config, which runs mcp-project-brain stdio --kb-repo=<owner/repo> over stdio. The repo is pre-filled from the project's connected GitHub repo.

This flow targets NeusisCode. For the binary, install scripts, and full reference, see docs.neusis.ai/docs/project-brain-mcp.

Reference files

ConcernFile
Provider typespackages/integrations/src/types.ts
Server registrypackages/integrations/src/registry.ts
UI registrypackages/integrations/src/ui/index.ts
Token lifecyclepackages/integrations/src/tokens.ts
Orphan helperpackages/integrations/src/sync/orphans.ts
Scope formapps/web/components/scope-form.tsx
Provider gridapps/web/app/(app)/projects/[slug]/integrations/page.tsx
Sources dashboardapps/web/app/(app)/projects/[slug]/sources/page.tsx
Dynamic scope pageapps/web/app/(app)/projects/[slug]/integrations/[provider]/page.tsx
Source detail pageapps/web/app/(app)/projects/[slug]/sources/[provider]/page.tsx
Connect routeapps/web/app/api/projects/[slug]/integrations/[product]/connect/route.ts
OAuth callbackapps/web/app/api/integrations/oauth/callback/route.ts
Worker syncapps/worker/src/workers/sync.ts