If you only integrate with GitHub, Octokit is the right tool and this article will not talk you out of it. The problem starts when the same product has to talk to GitLab, Bitbucket Cloud, Azure DevOps, or a self-hosted Gitea too, and you discover Octokit only speaks GitHub. From there you either wire up one SDK per provider and maintain five different clients, or you reach for a unified layer that normalizes all five behind one API.
This is an honest comparison of those paths: what Octokit does well, what the per-provider SDK landscape looks like, the real cost of running five SDKs side by side, and where a unified SDK helps versus where it gets in your way.
What does Octokit do well?
Octokit is GitHub’s official JavaScript SDK, and for GitHub work it is hard to beat. It tracks the REST API closely, so new endpoints and parameters show up quickly. It exposes GitHub’s GraphQL API, which matters once you need to fetch nested data in one round trip. It has first-class support for GitHub Apps, installation tokens, and app authentication flows. And its plugin system (throttling, retries, pagination) is mature and battle-tested.
None of that is a knock on Octokit. If your integration is GitHub-only, or GitHub-first with GitHub-specific depth, a dedicated SDK is the correct call. The tradeoff only appears when you add a second provider.
What does the per-provider SDK landscape look like?
Each provider has its own ecosystem, and the SDKs do not share a shape:
- GitLab — the community standard is
@gitbeaker/rest, a comprehensive client covering GitLab’s large API surface. - Bitbucket Cloud — the
bitbucketnpm package wraps Bitbucket’s REST API. - Azure DevOps — Microsoft’s
azure-devops-node-apiexposes the Azure DevOps services. - Gitea — the community
gitea-jsclient wraps Gitea’s/api/v1surface.
Each is a reasonable choice on its own. The difficulty is not any single library — it is that they were designed independently, so covering five providers means learning and maintaining five unrelated clients.
What does maintaining five SDKs actually cost?
The line count of “install five packages” hides where the time goes. The cost is in the seams between them:
- Five authentication models. GitHub tokens and GitHub Apps, GitLab PATs and project/group tokens, Bitbucket email-plus-API-token, Azure DevOps PATs or Entra ID, and Gitea’s
tokenheader scheme all differ in shape. Every one needs its own config, its own refresh story, and its own place to leak. - Five pagination styles. Link headers, page numbers, cursors, and single-response endpoints each need bespoke loop code, and the bugs live in the edge cases.
- Five error shapes. A 404 from one SDK is a thrown error, from another a response object, from another a rejected promise with a different property layout. Writing one retry or “resource missing” branch that works everywhere means normalizing all five by hand.
- Five mental models. Every provider names things differently — repos, projects, workspaces, namespaces — and every SDK surfaces those names differently again.
That glue code is code you write, test, and own forever. It is also where multi-provider integrations quietly rot.
Where does a unified SDK like repo-sdk fit?
A unified SDK collapses that glue into one normalized contract. repo-sdk is an open-source, zero-dependency TypeScript SDK that gives you a single API over GitHub, GitLab, Bitbucket Cloud, Azure DevOps, and Gitea (including Forgejo). You write your discovery, commit, tag, download, and webhook logic once, and switching providers means swapping one argument:
import { createClient } from 'repo-sdk';
import { github } from 'repo-sdk/github';
const client = createClient({
provider: github({ auth: { token: process.env.GITHUB_TOKEN! } }),
});
const head = await client.commits.get({
repo: 'capawesome-team/repo-sdk',
ref: 'main',
});
console.log(head.sha, head.message);
Point the same code at GitLab by changing only the provider factory:
import { gitlab } from 'repo-sdk/gitlab';
const client = createClient({
provider: gitlab({ auth: { token: process.env.GITLAB_TOKEN! } }),
});
The five cost centers above become one each. Authentication is one auth block per factory. Pagination is handled by listAll async iterators that follow cursors for you. Errors all throw a single RepoError with a normalized code union — unauthorized, not_found, rate_limited, unsupported, and so on — so one switch statement covers every provider:
import { RepoError } from 'repo-sdk';
try {
await client.repos.get({ repo: 'capawesome-team/repo-sdk' });
} catch (error) {
if (error instanceof RepoError && error.code === 'rate_limited') {
// back off using error.retryAfter
}
}
RepoError also redacts known secrets from its message before construction, so a token does not leak into your logs. Because the SDK is built on raw fetch and Web Crypto with zero runtime dependencies, it runs on Node.js 20 and up, Cloudflare Workers, and other Web-standard runtimes. Webhook verification and parsing are standalone exports that accept a Web-standard Request, so they drop into a Next.js route handler or a worker without a client instance. For the full picture of how the client and provider factories fit together, see Client and providers.
Comparison at a glance
| Octokit | Per-provider SDKs | repo-sdk | |
|---|---|---|---|
| Providers covered | GitHub only | One each (five packages) | GitHub, GitLab, Bitbucket Cloud, Azure DevOps, Gitea |
| Dependencies | Has dependencies | Varies per package | Zero runtime dependencies |
| Runtime support | Node and edge | Varies per package | Node 20+, Cloudflare Workers, Web-standard runtimes |
| Typed errors | GitHub-shaped | Five different shapes | One RepoError with a normalized code union |
| Webhook helpers | GitHub-specific | Varies per package | Verify and parse on each provider subpath |
When is Octokit or a dedicated SDK still the right choice?
A unified layer normalizes to the common ground across five providers, and that normalization is exactly what you do not want in some cases. Reach for Octokit or a dedicated provider SDK when:
- You need GraphQL. repo-sdk normalizes REST operations; it does not wrap GitHub’s GraphQL API.
- You need deep, provider-specific features. GitHub Apps installation flows, GitLab merge-request internals, Azure DevOps work items, and similar depth live in the dedicated SDKs. A common-denominator API cannot expose all of it.
- You are GitHub-only and expect to stay that way. The unified abstraction is overhead you would not use.
- You need an endpoint the unified SDK does not model. repo-sdk covers namespaces, repositories, commits, tags, and webhooks. Anything outside that surface belongs in a dedicated client.
repo-sdk is also honest about provider gaps rather than papering over them. Providers genuinely differ — Azure DevOps has no repo search, Bitbucket Cloud is Cloud-only, GitHub tags carry no date — and instead of silently dropping an unsupported option, the client throws unsupported. That is a feature for correctness, but it also means the abstraction has real edges, documented in the provider support matrix. Note too that repo-sdk is 0.x, so its API may change between minor releases.
The two approaches are not mutually exclusive. A common pattern is a unified SDK for the shared operations across every provider, plus a dedicated SDK dropped in for the one provider where you need depth.
FAQ
Can repo-sdk fully replace Octokit?
No, and it is not meant to. It replaces the multi-provider glue code — the shared discovery, commit, tag, download, and webhook operations. For GitHub GraphQL or deep GitHub App features, you still want Octokit.
Does repo-sdk work in Cloudflare Workers and other edge runtimes?
Yes. It has zero runtime dependencies and is built on raw fetch and Web Crypto, so it runs on Node.js 20 and up, Cloudflare Workers, and other Web-standard runtimes.
How does repo-sdk handle errors differently from five separate SDKs?
Every failure throws a single RepoError with a normalized code union, plus retryable and retryAfter hints and secret redaction. One error branch works across all five providers instead of five provider-specific ones. See Errors.