Contributing Providers & Integrations¶
The two extensions that must live in the doable repo (rather than per-workspace) are AI providers (Anthropic, OpenAI, Copilot, your own LLM) and integrations (Linear, Gmail, Stripe, ...). They touch shared code, ship with the platform, and need PR review.
This page is the tutorial-style "you, the contributor" view. For the field-by-field reference, see the existing contributing pages:
What's the difference?¶
| AI Provider | Integration | |
|---|---|---|
| What it adds | A new LLM the user can pick in their workspace settings | A third-party API exposed to the AI as tools |
| Examples | Claude, GPT-5, Llama via Ollama, Copilot CLI | Linear, Notion, Stripe, GitHub, Gmail |
| Where the code lives | services/api/src/ai/providers/ |
services/api/src/integrations/registry/<category>.ts |
| Registered via | providers array in providers/index.ts |
integrations array in registry/index.ts |
| User-facing surface | Workspace Settings → AI → model picker | Workspace Settings → Integrations |
Most contributions are integrations: that's where the catalog gap lives. Providers are rarer but high-leverage; one provider unlocks every model the vendor offers.
Before you start¶
- Read the project's Contributing Guide and Conventions.
- Check GitHub issues for an existing request, and please link your PR.
- Skim
anthropic.tsfor providers, or one of the bigger registry files (e.g.productivity.ts) for integrations. Match their shape.
1. Clone and set up¶
git clone https://github.com/doable-me/doable.git
cd doable
pnpm install
cp services/api/.env.example services/api/.env # fill in DB + OAuth bits
pnpm db:migrate
pnpm dev
The dev server runs at http://127.0.0.1:3000 with hot-reload via tsx watch for the API. See Local Development for the full setup if anything trips up.
2. Add a provider¶
The pattern is small: a single .ts module that exports an AIProvider object.
// services/api/src/ai/providers/myprovider.ts
import type { AIProvider, ChatRequest, AIEvent } from '../types.js';
export const myProvider: AIProvider = {
id: 'myprovider',
displayName: 'My Provider',
isAvailable: () => Boolean(process.env.MYPROVIDER_API_KEY),
models: [
{ id: 'myprovider:flash', name: 'My Flash', context: 128_000 },
{ id: 'myprovider:pro', name: 'My Pro', context: 1_000_000 },
],
async *stream(req: ChatRequest): AsyncIterable<AIEvent> {
// POST to your provider, parse SSE, yield uniform AIEvents
},
};
The minimum:
isAvailable(): returns true when the necessary env vars are present.models: the list shown in the picker; theidis the wire-format model name.stream(): translates the provider's wire events into Doable'sAIEventkinds (assistant.message_delta,tool.call,usage,done, …).
Then register it in services/api/src/ai/providers/index.ts:
import { myProvider } from './myprovider.js';
export const providers = [
// ... existing providers
myProvider,
];
And add the env var stub to services/api/.env.example so operators know it exists. For the per-event mapping table, error handling rules, and tool-call semantics, see Add an AI Provider. It's the field reference.
3. Add an integration¶
Integrations live under services/api/src/integrations/registry/, grouped by category (ai-ml.ts, productivity.ts, crm-marketing-social.ts, etc.). Pick the category file that fits, or create a new one and re-export from registry/index.ts.
A minimal integration:
// services/api/src/integrations/registry/productivity.ts
export const linearIntegration = {
id: 'linear',
name: 'Linear',
category: 'productivity',
description: 'Issue tracking for fast-moving teams.',
icon: '/integrations/linear.svg',
auth: {
type: 'oauth2',
authorizeUrl: 'https://linear.app/oauth/authorize',
tokenUrl: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
clientIdEnv: 'LINEAR_CLIENT_ID',
clientSecretEnv: 'LINEAR_CLIENT_SECRET',
},
tools: [
{
name: 'linear_create_issue',
description: 'Create an issue in a Linear team.',
parameters: {
type: 'object',
properties: {
team_id: { type: 'string' },
title: { type: 'string' },
body: { type: 'string' },
},
required: ['team_id', 'title'],
},
handler: async ({ team_id, title, body }, ctx) => {
const res = await ctx.fetchOAuth('linear',
'https://api.linear.app/graphql', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ /* GraphQL mutation */ }),
});
return await res.json();
},
},
],
};
Then register it and add the env vars. The full pattern, including tool naming convention (<integration_id>_<verb>_<object>), tool-permission policy (auto / ask / block), and ctx-helper docs (ctx.fetchOAuth, ctx.audit, …), lives in Add an Integration.
4. Validate¶
Every contribution needs to clear the same bar:
- Type-check.
pnpm typecheck: noanyleaks, no missing fields. - Lint.
pnpm lint: follow the codebase's import order and naming. - Test. Add a Vitest file under
__tests__/next to your module. Mockfetch(orfetchOAuth), feed a recorded SSE/HTTP response, assert the emitted events or returned shape. See Testing for the test-suite layout. - No secrets in the diff. Never commit
.env, OAuth client secrets, or recorded auth tokens.
5. Test locally¶
Spin up the app:
- For a provider: open Workspace Settings → AI, switch model to one of yours, send a chat message. Watch the API logs for the event stream and confirm
usagenumbers come through. - For an integration: open Workspace Settings → Integrations, find your entry, click Connect, complete the OAuth dance, then in chat ask the AI to use the tool. Watch the API logs to confirm the handler fires.
6. Open the PR¶
git checkout -b add-myprovider # or add-linear-integration
git add services/api/src/ai/providers/myprovider.ts \
services/api/src/ai/providers/index.ts \
services/api/.env.example
git commit -m "feat(ai): add MyProvider"
git push origin add-myprovider
gh pr create --fill
A good PR description includes:
- What the provider/integration does.
- Why it's worth shipping platform-wide (the bar: meaningful user count or a category gap).
- Env vars added (so operators know what to set).
- Test evidence: a screenshot of the picker / connect flow, or a transcript of the AI using the tool.
Maintainers will check that streaming works, errors surface as events (not swallowed), tool calls round-trip cleanly, and the env-var story is documented.
7. After it merges¶
Once your change is on main, it ships with the next Doable release. Operators upgrade, set their env vars, and your work is live everywhere.
If your integration depends on a third-party OAuth app, document the redirect URI in your PR so others can configure their own client. The generic OAuth flow in services/api/src/integrations/oauth.ts handles the rest as long as the auth block follows the schema.
Related¶
- Add an AI Provider: field-by-field reference
- Add an Integration: field-by-field reference
- Conventions: code style, file layout, naming
- Testing: how the test suite is organized
- Contributing Guide: PR process and review expectations