diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts
index 29f35185311300193ae6187d089878ce1f6cd1c4..c5cbbea651b770f37df939b6f8bad545d2a9ebf7 100644
--- a/lib/modules/manager/api.ts
+++ b/lib/modules/manager/api.ts
@@ -11,6 +11,7 @@ import * as bazelisk from './bazelisk';
 import * as bicep from './bicep';
 import * as bitbucketPipelines from './bitbucket-pipelines';
 import * as buildkite from './buildkite';
+import * as bun from './bun';
 import * as bundler from './bundler';
 import * as cake from './cake';
 import * as cargo from './cargo';
@@ -101,6 +102,7 @@ api.set('bazelisk', bazelisk);
 api.set('bicep', bicep);
 api.set('bitbucket-pipelines', bitbucketPipelines);
 api.set('buildkite', buildkite);
+api.set('bun', bun);
 api.set('bundler', bundler);
 api.set('cake', cake);
 api.set('cargo', cargo);
diff --git a/lib/modules/manager/bun/artifacts.spec.ts b/lib/modules/manager/bun/artifacts.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..92f23ac660b7b764568659dcd9a77ebe9c2eb326
--- /dev/null
+++ b/lib/modules/manager/bun/artifacts.spec.ts
@@ -0,0 +1,129 @@
+import _fs from 'fs-extra';
+import { mocked } from '../../../../test/util';
+import { GlobalConfig } from '../../../config/global';
+import type { RepoGlobalConfig } from '../../../config/types';
+import { TEMPORARY_ERROR } from '../../../constants/error-messages';
+import { exec as _exec } from '../../../util/exec';
+import { ExecError } from '../../../util/exec/exec-error';
+import type { UpdateArtifact } from '../types';
+import { updateArtifacts } from './artifacts';
+
+jest.mock('../../../util/exec');
+jest.mock('fs-extra');
+
+const exec = mocked(_exec);
+const fs = mocked(_fs);
+
+const globalConfig: RepoGlobalConfig = {
+  localDir: '',
+};
+
+describe('modules/manager/bun/artifacts', () => {
+  describe('updateArtifacts()', () => {
+    let updateArtifact: UpdateArtifact;
+
+    beforeEach(() => {
+      GlobalConfig.set(globalConfig);
+      updateArtifact = {
+        config: {},
+        newPackageFileContent: '',
+        packageFileName: '',
+        updatedDeps: [],
+      };
+    });
+
+    it('skips if no updatedDeps and no lockFileMaintenance', async () => {
+      expect(await updateArtifacts(updateArtifact)).toBeNull();
+    });
+
+    it('skips if no lock file in config', async () => {
+      updateArtifact.updatedDeps = [{}];
+      expect(await updateArtifacts(updateArtifact)).toBeNull();
+    });
+
+    it('skips if cannot read lock file', async () => {
+      updateArtifact.updatedDeps = [{}];
+      updateArtifact.config.lockFiles = ['bun.lockb'];
+      expect(await updateArtifacts(updateArtifact)).toBeNull();
+    });
+
+    it('returns null if lock content unchanged', async () => {
+      updateArtifact.updatedDeps = [{}];
+      updateArtifact.config.lockFiles = ['bun.lockb'];
+      const oldLock = Buffer.from('old');
+      fs.readFile.mockResolvedValueOnce(oldLock as never);
+      fs.readFile.mockResolvedValueOnce(oldLock as never);
+      expect(await updateArtifacts(updateArtifact)).toBeNull();
+    });
+
+    it('returns updated lock content', async () => {
+      updateArtifact.updatedDeps = [{}];
+      updateArtifact.config.lockFiles = ['bun.lockb'];
+      const oldLock = Buffer.from('old');
+      fs.readFile.mockResolvedValueOnce(oldLock as never);
+      const newLock = Buffer.from('new');
+      fs.readFile.mockResolvedValueOnce(newLock as never);
+      expect(await updateArtifacts(updateArtifact)).toEqual([
+        {
+          file: {
+            path: 'bun.lockb',
+            type: 'addition',
+            contents: newLock,
+          },
+        },
+      ]);
+    });
+
+    it('supports lockFileMaintenance', async () => {
+      updateArtifact.config.lockFiles = ['bun.lockb'];
+      updateArtifact.config.updateType = 'lockFileMaintenance';
+      const oldLock = Buffer.from('old');
+      fs.readFile.mockResolvedValueOnce(oldLock as never);
+      const newLock = Buffer.from('new');
+      fs.readFile.mockResolvedValueOnce(newLock as never);
+      expect(await updateArtifacts(updateArtifact)).toEqual([
+        {
+          file: {
+            path: 'bun.lockb',
+            type: 'addition',
+            contents: newLock,
+          },
+        },
+      ]);
+    });
+
+    it('handles temporary error', async () => {
+      const execError = new ExecError(TEMPORARY_ERROR, {
+        cmd: '',
+        stdout: '',
+        stderr: '',
+        options: { encoding: 'utf8' },
+      });
+      updateArtifact.updatedDeps = [{}];
+      updateArtifact.config.lockFiles = ['bun.lockb'];
+      const oldLock = Buffer.from('old');
+      fs.readFile.mockResolvedValueOnce(oldLock as never);
+      exec.mockRejectedValueOnce(execError);
+      await expect(updateArtifacts(updateArtifact)).rejects.toThrow(
+        TEMPORARY_ERROR
+      );
+    });
+
+    it('handles full error', async () => {
+      const execError = new ExecError('nope', {
+        cmd: '',
+        stdout: '',
+        stderr: '',
+        options: { encoding: 'utf8' },
+      });
+      updateArtifact.updatedDeps = [{}];
+      updateArtifact.config.lockFiles = ['bun.lockb'];
+      const oldLock = Buffer.from('old');
+      fs.readFile.mockResolvedValueOnce(oldLock as never);
+      exec.mockRejectedValueOnce(execError);
+      expect(await updateArtifacts(updateArtifact)).toEqual([
+        { artifactError: { lockFile: 'bun.lockb', stderr: 'nope' } },
+      ]);
+    });
+  });
+});
diff --git a/lib/modules/manager/bun/artifacts.ts b/lib/modules/manager/bun/artifacts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7fdef867e659db8e4e86db7f032d23dd2a9336d2
--- /dev/null
+++ b/lib/modules/manager/bun/artifacts.ts
@@ -0,0 +1,87 @@
+import is from '@sindresorhus/is';
+import { TEMPORARY_ERROR } from '../../../constants/error-messages';
+import { logger } from '../../../logger';
+import { exec } from '../../../util/exec';
+import type { ExecOptions } from '../../../util/exec/types';
+import {
+  deleteLocalFile,
+  readLocalFile,
+  writeLocalFile,
+} from '../../../util/fs';
+import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
+
+export async function updateArtifacts(
+  updateArtifact: UpdateArtifact
+): Promise<UpdateArtifactsResult[] | null> {
+  const { packageFileName, updatedDeps, newPackageFileContent, config } =
+    updateArtifact;
+  logger.debug({ updateArtifact }, `bun.updateArtifacts(${packageFileName})`);
+  const isLockFileMaintenance = config.updateType === 'lockFileMaintenance';
+
+  if (is.emptyArray(updatedDeps) && !isLockFileMaintenance) {
+    logger.debug('No updated bun deps - returning null');
+    return null;
+  }
+
+  const lockFileName = config.lockFiles?.[0];
+
+  if (!lockFileName) {
+    logger.debug(`No ${lockFileName} found`);
+    return null;
+  }
+
+  const oldLockFileContent = await readLocalFile(lockFileName);
+  if (!oldLockFileContent) {
+    logger.debug(`No ${lockFileName} found`);
+    return null;
+  }
+
+  try {
+    await writeLocalFile(packageFileName, newPackageFileContent);
+    if (isLockFileMaintenance) {
+      await deleteLocalFile(lockFileName);
+    }
+
+    const execOptions: ExecOptions = {
+      cwdFile: packageFileName,
+      docker: {},
+      toolConstraints: [
+        {
+          toolName: 'bun',
+          constraint: updateArtifact?.config?.constraints?.bun,
+        },
+      ],
+    };
+
+    await exec('bun install', execOptions);
+    const newLockFileContent = await readLocalFile(lockFileName);
+    if (
+      !newLockFileContent ||
+      Buffer.compare(oldLockFileContent, newLockFileContent) === 0
+    ) {
+      return null;
+    }
+    return [
+      {
+        file: {
+          type: 'addition',
+          path: lockFileName,
+          contents: newLockFileContent,
+        },
+      },
+    ];
+  } catch (err) {
+    if (err.message === TEMPORARY_ERROR) {
+      throw err;
+    }
+    logger.warn({ lockfile: lockFileName, err }, `Failed to update lock file`);
+    return [
+      {
+        artifactError: {
+          lockFile: lockFileName,
+          stderr: err.message,
+        },
+      },
+    ];
+  }
+}
diff --git a/lib/modules/manager/bun/extract.spec.ts b/lib/modules/manager/bun/extract.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15411f0f492f3f6a67aff9df8531354c6144bccf
--- /dev/null
+++ b/lib/modules/manager/bun/extract.spec.ts
@@ -0,0 +1,68 @@
+import { fs } from '../../../../test/util';
+import { extractAllPackageFiles } from './extract';
+
+jest.mock('../../../util/fs');
+
+describe('modules/manager/bun/extract', () => {
+  describe('extractAllPackageFiles()', () => {
+    it('ignores non-bun files', async () => {
+      expect(await extractAllPackageFiles({}, ['package.json'])).toEqual([]);
+    });
+
+    it('ignores missing package.json file', async () => {
+      expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]);
+    });
+
+    it('ignores invalid package.json file', async () => {
+      (fs.readLocalFile as jest.Mock).mockResolvedValueOnce('invalid');
+      expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]);
+    });
+
+    it('handles null response', async () => {
+      fs.getSiblingFileName.mockReturnValueOnce('package.json');
+      fs.readLocalFile.mockResolvedValueOnce(
+        // This package.json returns null from the extractor
+        JSON.stringify({
+          _id: 1,
+          _args: 1,
+          _from: 1,
+        })
+      );
+      expect(await extractAllPackageFiles({}, ['bun.lockb'])).toEqual([]);
+    });
+
+    it('parses valid package.json file', async () => {
+      fs.getSiblingFileName.mockReturnValueOnce('package.json');
+      fs.readLocalFile.mockResolvedValueOnce(
+        JSON.stringify({
+          name: 'test',
+          version: '0.0.1',
+          dependencies: {
+            dep1: '1.0.0',
+          },
+        })
+      );
+      expect(await extractAllPackageFiles({}, ['bun.lockb'])).toMatchObject([
+        {
+          deps: [
+            {
+              currentValue: '1.0.0',
+              datasource: 'npm',
+              depName: 'dep1',
+              depType: 'dependencies',
+              prettyDepType: 'dependency',
+            },
+          ],
+          extractedConstraints: {},
+          lockFiles: ['bun.lockb'],
+          managerData: {
+            hasPackageManager: false,
+            packageJsonName: 'test',
+          },
+          packageFile: 'package.json',
+          packageFileVersion: '0.0.1',
+        },
+      ]);
+    });
+  });
+});
diff --git a/lib/modules/manager/bun/extract.ts b/lib/modules/manager/bun/extract.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd0514f122f34292d030e1f9ad56150934a34565
--- /dev/null
+++ b/lib/modules/manager/bun/extract.ts
@@ -0,0 +1,53 @@
+import { logger } from '../../../logger';
+import { getSiblingFileName, readLocalFile } from '../../../util/fs';
+
+import { extractPackageJson } from '../npm/extract/common/package-file';
+import type { NpmPackage } from '../npm/extract/types';
+import type { NpmManagerData } from '../npm/types';
+import type { ExtractConfig, PackageFile } from '../types';
+
+function matchesFileName(fileNameWithPath: string, fileName: string): boolean {
+  return (
+    fileNameWithPath === fileName || fileNameWithPath.endsWith(`/${fileName}`)
+  );
+}
+
+export async function extractAllPackageFiles(
+  config: ExtractConfig,
+  matchedFiles: string[]
+): Promise<PackageFile[]> {
+  const packageFiles: PackageFile<NpmManagerData>[] = [];
+  for (const matchedFile of matchedFiles) {
+    if (!matchesFileName(matchedFile, 'bun.lockb')) {
+      logger.warn({ matchedFile }, 'Invalid bun lockfile match');
+      continue;
+    }
+    const packageFile = getSiblingFileName(matchedFile, 'package.json');
+    const packageFileContent = await readLocalFile(packageFile, 'utf8');
+    if (!packageFileContent) {
+      logger.debug({ packageFile }, 'No package.json found');
+      continue;
+    }
+
+    let packageJson: NpmPackage;
+    try {
+      packageJson = JSON.parse(packageFileContent);
+    } catch (err) {
+      logger.debug({ err }, 'Error parsing package.json');
+      continue;
+    }
+
+    const extracted = extractPackageJson(packageJson, packageFile);
+    if (!extracted) {
+      logger.debug({ packageFile }, 'No dependencies found');
+      continue;
+    }
+    packageFiles.push({
+      ...extracted,
+      packageFile,
+      lockFiles: [matchedFile],
+    });
+  }
+
+  return packageFiles;
+}
diff --git a/lib/modules/manager/bun/index.ts b/lib/modules/manager/bun/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..22ad5d6ce1212d87c07d9e5c65d9fe7ef83e5018
--- /dev/null
+++ b/lib/modules/manager/bun/index.ts
@@ -0,0 +1,30 @@
+import type { Category } from '../../../constants';
+import { GithubTagsDatasource } from '../../datasource/github-tags';
+import { NpmDatasource } from '../../datasource/npm';
+import * as npmVersioning from '../../versioning/npm';
+
+export { updateArtifacts } from './artifacts';
+export { extractAllPackageFiles } from './extract';
+export { getRangeStrategy, updateDependency } from '../npm';
+
+export const supersedesManagers = ['npm'];
+export const supportsLockFileMaintenance = true;
+
+export const defaultConfig = {
+  fileMatch: ['(^|/)bun\\.lockb$'],
+  versioning: npmVersioning.id,
+  digest: {
+    prBodyDefinitions: {
+      Change:
+        '{{#if displayFrom}}`{{{displayFrom}}}` -> {{else}}{{#if currentValue}}`{{{currentValue}}}` -> {{/if}}{{/if}}{{#if displayTo}}`{{{displayTo}}}`{{else}}`{{{newValue}}}`{{/if}}',
+    },
+  },
+  prBodyDefinitions: {
+    Change:
+      "[{{#if displayFrom}}`{{{displayFrom}}}` -> {{else}}{{#if currentValue}}`{{{currentValue}}}` -> {{/if}}{{/if}}{{#if displayTo}}`{{{displayTo}}}`{{else}}`{{{newValue}}}`{{/if}}]({{#if depName}}https://renovatebot.com/diffs/npm/{{replace '/' '%2f' depName}}/{{{currentVersion}}}/{{{newVersion}}}{{/if}})",
+  },
+};
+
+export const categories: Category[] = ['js'];
+
+export const supportedDatasources = [GithubTagsDatasource.id, NpmDatasource.id];
diff --git a/lib/modules/manager/bun/readme.md b/lib/modules/manager/bun/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..0fa2f420f7fb95db97e16f2a57a55f4e0a987909
--- /dev/null
+++ b/lib/modules/manager/bun/readme.md
@@ -0,0 +1,5 @@
+Used for updating bun projects.
+Bun is a tool for JavaScript projects and therefore an alternative to managers like npm, pnpm and Yarn.
+
+If a `package.json` is found to be part of `bun` manager results then the same file will be excluded from the `npm` manager results so that it's not duplicated.
+This means that supporting a `bun.lockb` file in addition to other JS lock files is not supported - Bun will take priority.
diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts
index 8cfdba235b92ee67b1ff908940e563fa2a0f7146..ad5b615a6517e39c68f6c1a6a47960f87425378a 100644
--- a/lib/modules/manager/types.ts
+++ b/lib/modules/manager/types.ts
@@ -37,6 +37,7 @@ export interface UpdateArtifactsConfig {
   newVersion?: string;
   newMajor?: number;
   registryAliases?: Record<string, string>;
+  lockFiles?: string[];
 }
 
 export interface RangeConfig<T = Record<string, any>> extends ManagerData<T> {
@@ -226,7 +227,7 @@ export interface ManagerApi extends ModuleApi {
 
   categories?: Category[];
   supportsLockFileMaintenance?: boolean;
-
+  supersedesManagers?: string[];
   supportedDatasources: string[];
 
   bumpPackageVersion?(
diff --git a/lib/util/exec/containerbase.ts b/lib/util/exec/containerbase.ts
index 6571d1850b8b98df05caa7ac21cd136b2e798c32..c643536b3df27f50ac8840823a8f8a8efa91948e 100644
--- a/lib/util/exec/containerbase.ts
+++ b/lib/util/exec/containerbase.ts
@@ -17,6 +17,12 @@ import { id as semverCoercedVersioningId } from '../../modules/versioning/semver
 import type { Opt, ToolConfig, ToolConstraint } from './types';
 
 const allToolConfig: Record<string, ToolConfig> = {
+  bun: {
+    datasource: 'github-releases',
+    packageName: 'oven-sh/bun',
+    extractVersion: '^bun-v(?<version>.*)$',
+    versioning: npmVersioningId,
+  },
   bundler: {
     datasource: 'rubygems',
     packageName: 'bundler',
diff --git a/lib/workers/repository/extract/index.ts b/lib/workers/repository/extract/index.ts
index bcf281bb5f9071e8a86c6ba92c7f8f6c28b117cb..04cdc3200a29a35b1ce84402aa3aa62ef6813e03 100644
--- a/lib/workers/repository/extract/index.ts
+++ b/lib/workers/repository/extract/index.ts
@@ -8,6 +8,7 @@ import { scm } from '../../../modules/platform/scm';
 import type { ExtractResult, WorkerExtractConfig } from '../../types';
 import { getMatchingFiles } from './file-match';
 import { getManagerPackageFiles } from './manager-files';
+import { processSupersedesManagers } from './supersedes';
 
 export async function extractAllDependencies(
   config: RenovateConfig
@@ -66,6 +67,10 @@ export async function extractAllDependencies(
       return { manager: managerConfig.manager, packageFiles };
     })
   );
+
+  // De-duplicate results using supersedesManagers
+  processSupersedesManagers(extractResults);
+
   logger.debug(
     { managers: extractDurations },
     'manager extract durations (ms)'
diff --git a/lib/workers/repository/extract/supersedes.spec.ts b/lib/workers/repository/extract/supersedes.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..247b74e52a067857befb65d6ad0b787540abc6ba
--- /dev/null
+++ b/lib/workers/repository/extract/supersedes.spec.ts
@@ -0,0 +1,46 @@
+import { processSupersedesManagers } from './supersedes';
+import type { ExtractResults } from './types';
+
+describe('workers/repository/extract/supersedes', () => {
+  describe('processSupercedesManagers', () => {
+    it('handles empty extractResults', () => {
+      const extractResults: ExtractResults[] = [];
+      processSupersedesManagers(extractResults);
+      expect(extractResults).toHaveLength(0);
+    });
+
+    it('handles supercedes subset', () => {
+      const extractResults: ExtractResults[] = [
+        { manager: 'ansible' },
+        {
+          manager: 'bun',
+          packageFiles: [{ packageFile: 'package.json', deps: [] }],
+        },
+        {
+          manager: 'npm',
+          packageFiles: [
+            { packageFile: 'package.json', deps: [] },
+            { packageFile: 'backend/package.json', deps: [] },
+          ],
+        },
+      ];
+      processSupersedesManagers(extractResults);
+      expect(extractResults).toMatchObject([
+        { manager: 'ansible' },
+        {
+          manager: 'bun',
+          packageFiles: [
+            {
+              deps: [],
+              packageFile: 'package.json',
+            },
+          ],
+        },
+        {
+          manager: 'npm',
+          packageFiles: [{ deps: [], packageFile: 'backend/package.json' }],
+        },
+      ]);
+    });
+  });
+});
diff --git a/lib/workers/repository/extract/supersedes.ts b/lib/workers/repository/extract/supersedes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..560d6bb0c50d65d460638c18130d4b60dbac4567
--- /dev/null
+++ b/lib/workers/repository/extract/supersedes.ts
@@ -0,0 +1,35 @@
+import is from '@sindresorhus/is';
+import { get } from '../../../modules/manager';
+import type { ExtractResults } from './types';
+
+export function processSupersedesManagers(
+  extractResults: ExtractResults[]
+): void {
+  for (const { manager, packageFiles } of extractResults) {
+    if (!packageFiles) {
+      continue;
+    }
+    const supersedesManagers = get(manager, 'supersedesManagers');
+    if (is.nonEmptyArray(supersedesManagers)) {
+      const supercedingPackageFileNames = packageFiles.map(
+        (packageFile) => packageFile.packageFile
+      );
+      for (const supercededManager of supersedesManagers) {
+        const supercededManagerResults = extractResults.find(
+          (result) => result.manager === supercededManager
+        );
+        if (supercededManagerResults?.packageFiles) {
+          supercededManagerResults.packageFiles =
+            supercededManagerResults.packageFiles.filter((packageFile) => {
+              if (
+                supercedingPackageFileNames.includes(packageFile.packageFile)
+              ) {
+                return false;
+              }
+              return true;
+            });
+        }
+      }
+    }
+  }
+}
diff --git a/lib/workers/repository/extract/types.ts b/lib/workers/repository/extract/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4526d46e8f8b54f3cd466d9ee4d9d1b37ac217b1
--- /dev/null
+++ b/lib/workers/repository/extract/types.ts
@@ -0,0 +1,6 @@
+import type { PackageFile } from '../../../modules/manager/types';
+
+export interface ExtractResults {
+  manager: string;
+  packageFiles?: PackageFile[] | null;
+}