diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts
index a9839f4f7a11deaa145a9077706d201581b87bd3..e7284442c0a36ba4638f81e168f6f93e81e45c5c 100644
--- a/lib/modules/datasource/api.ts
+++ b/lib/modules/datasource/api.ts
@@ -38,6 +38,7 @@ import { GlasskubePackagesDatasource } from './glasskube-packages';
 import { GoDatasource } from './go';
 import { GolangVersionDatasource } from './golang-version';
 import { GradleVersionDatasource } from './gradle-version';
+import { HackageDatasource } from './hackage';
 import { HelmDatasource } from './helm';
 import { HermitDatasource } from './hermit';
 import { HexDatasource } from './hex';
@@ -111,6 +112,7 @@ api.set(GlasskubePackagesDatasource.id, new GlasskubePackagesDatasource());
 api.set(GoDatasource.id, new GoDatasource());
 api.set(GolangVersionDatasource.id, new GolangVersionDatasource());
 api.set(GradleVersionDatasource.id, new GradleVersionDatasource());
+api.set(HackageDatasource.id, new HackageDatasource());
 api.set(HelmDatasource.id, new HelmDatasource());
 api.set(HermitDatasource.id, new HermitDatasource());
 api.set(HexDatasource.id, new HexDatasource());
diff --git a/lib/modules/datasource/hackage/index.spec.ts b/lib/modules/datasource/hackage/index.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..676e082583da4895d42886025bc434db338467f3
--- /dev/null
+++ b/lib/modules/datasource/hackage/index.spec.ts
@@ -0,0 +1,57 @@
+import { getPkgReleases } from '..';
+import * as httpMock from '../../../../test/http-mock';
+import { HackageDatasource, versionToRelease } from './index';
+
+const baseUrl = 'https://hackage.haskell.org/';
+
+describe('modules/datasource/hackage/index', () => {
+  describe('versionToRelease', () => {
+    it('should make release with given version', () => {
+      expect(
+        versionToRelease('3.1.0', 'base', 'http://localhost').version,
+      ).toBe('3.1.0');
+    });
+  });
+
+  describe('getReleases', () => {
+    it('return null with empty registryUrl', async () => {
+      expect(
+        await new HackageDatasource().getReleases({
+          packageName: 'base',
+          registryUrl: undefined,
+        }),
+      ).toBeNull();
+    });
+
+    it('returns null for 404', async () => {
+      httpMock.scope(baseUrl).get('/package/base.json').reply(404);
+      expect(
+        await getPkgReleases({
+          datasource: HackageDatasource.id,
+          packageName: 'base',
+        }),
+      ).toBeNull();
+    });
+
+    it('returns release for 200', async () => {
+      httpMock
+        .scope(baseUrl)
+        .get('/package/base.json')
+        .reply(200, { '4.20.0.1': 'normal' });
+      expect(
+        await getPkgReleases({
+          datasource: HackageDatasource.id,
+          packageName: 'base',
+        }),
+      ).toEqual({
+        registryUrl: baseUrl,
+        releases: [
+          {
+            changelogUrl: baseUrl + 'package/base-4.20.0.1/changelog',
+            version: '4.20.0.1',
+          },
+        ],
+      });
+    });
+  });
+});
diff --git a/lib/modules/datasource/hackage/index.ts b/lib/modules/datasource/hackage/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4a75568d485468af32dc7824df6e4041cbd4c471
--- /dev/null
+++ b/lib/modules/datasource/hackage/index.ts
@@ -0,0 +1,54 @@
+import is from '@sindresorhus/is';
+import { joinUrlParts } from '../../../util/url';
+import * as pvpVersioning from '../../versioning/pvp';
+import { Datasource } from '../datasource';
+import type { GetReleasesConfig, Release, ReleaseResult } from '../types';
+import { HackagePackageMetadata } from './schema';
+
+export class HackageDatasource extends Datasource {
+  static readonly id = 'hackage';
+
+  constructor() {
+    super(HackageDatasource.id);
+  }
+
+  override readonly defaultVersioning = pvpVersioning.id;
+  override readonly customRegistrySupport = false;
+  override readonly defaultRegistryUrls = ['https://hackage.haskell.org/'];
+
+  async getReleases(config: GetReleasesConfig): Promise<ReleaseResult | null> {
+    const { registryUrl, packageName } = config;
+    if (!is.nonEmptyString(registryUrl)) {
+      return null;
+    }
+    const massagedPackageName = encodeURIComponent(packageName);
+    const url = joinUrlParts(
+      registryUrl,
+      'package',
+      `${massagedPackageName}.json`,
+    );
+    const res = await this.http.getJson(url, HackagePackageMetadata);
+    const keys = Object.keys(res.body);
+    return {
+      releases: keys.map((version) =>
+        versionToRelease(version, packageName, registryUrl),
+      ),
+    };
+  }
+}
+
+export function versionToRelease(
+  version: string,
+  packageName: string,
+  registryUrl: string,
+): Release {
+  return {
+    version,
+    changelogUrl: joinUrlParts(
+      registryUrl,
+      'package',
+      `${packageName}-${version}`,
+      'changelog',
+    ),
+  };
+}
diff --git a/lib/modules/datasource/hackage/readme.md b/lib/modules/datasource/hackage/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7e56e14b844c1a1176d8f040605185ff3a73be7
--- /dev/null
+++ b/lib/modules/datasource/hackage/readme.md
@@ -0,0 +1,7 @@
+This datasource uses
+[the Hackage JSON API](https://hackage.haskell.org/api#package-info-json)
+to fetch versions for published Haskell packages.
+
+While not all versions use [PVP](https://pvp.haskell.org), the majority does.
+This manager assumes a default versioning set to PVP.
+Versioning can be overwritten using `packageRules`, e.g. with `matchDatasources`.
diff --git a/lib/modules/datasource/hackage/schema.ts b/lib/modules/datasource/hackage/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dcee186743c6020406b676449bee6abffa1fb198
--- /dev/null
+++ b/lib/modules/datasource/hackage/schema.ts
@@ -0,0 +1,3 @@
+import { z } from 'zod';
+
+export const HackagePackageMetadata = z.record(z.string());