diff --git a/badgers-web/src/utils/Codeberg.ts b/badgers-web/src/utils/Codeberg.ts
new file mode 100644
index 0000000..f8268a2
--- /dev/null
+++ b/badgers-web/src/utils/Codeberg.ts
@@ -0,0 +1,74 @@
+const API_BASE = 'https://codeberg.org/api/v1';
+
+type ProjectInfo = {
+ owner: string
+ repo: string
+}
+
+type Repository = {
+ id: number
+ default_branch: string
+ name: string
+ forks_count: number
+ stars_count: number
+}
+
+type Release = {
+ name: string
+ tag_name: string
+}
+
+class CodebergClient {
+ token: string
+
+ constructor(token: string) {
+ this.token = token
+ }
+
+ buildUrl(path: string, query: Record = {}): string {
+ const queryArgs = {
+ ...query,
+ token: this.token,
+ }
+ const queryString = Object
+ .entries(queryArgs)
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
+ .join('&')
+
+ return `${API_BASE}/${path}?${queryString}`
+ }
+
+ async getRepository({ owner, repo }: ProjectInfo): Promise {
+ const repoId = `${owner}/${repo}`
+ const url = this.buildUrl(`repos/${repoId}`)
+ const resp = await fetch(url)
+
+ if (resp.status !== 200) return null
+ return await resp.json() as Repository
+ }
+
+ async getIssuesCount({ owner, repo }: ProjectInfo, query: Record = {}): Promise {
+ const repoId = `${owner}/${repo}`
+ const url = this.buildUrl(`repos/${repoId}/issues`, query)
+ const resp = await fetch(url)
+
+ if (resp.status !== 200) return null
+ const count = resp.headers.get('x-total-count')
+ return Number(count)
+ }
+
+ async getLatestRelease({ owner, repo }: ProjectInfo): Promise {
+ const repoId = `${owner}/${repo}`
+ const url = this.buildUrl(`repos/${repoId}/releases/latest`)
+ const resp = await fetch(url)
+
+ if (resp.status !== 200) return null
+ return await resp.json() as Release
+ }
+}
+
+export default class Codeberg {
+ static getClient(): CodebergClient {
+ return new CodebergClient(process.env.CODEBERG_TOKEN as string)
+ }
+}