--- template/app/src/landing-page/operations.ts
+++ opensaas-sh/app/src/landing-page/operations.ts
@@ -0,0 +1,247 @@
+import type { GetGithubRoadmap } from "wasp/server/operations";
+
+import * as z from "zod";
+import { assertUnreachable } from "../shared/utils";
+
+const GITHUB_ORG_NAME = "wasp-lang";
+const OPEN_SAAS_GITHUB_PROJECT_NUMBER = 6;
+
+export enum GithubEpicStatus {
+  Ideas = "Ideas",
+  Planned = "Planned",
+  InProgress = "In progress",
+  Done = "Done",
+}
+
+export type GithubEpic = {
+  name: string;
+  totalIssues: number;
+  doneIssues: number;
+  status: GithubEpicStatus;
+  url: string;
+};
+
+let cachedGithubEpicsData: GithubEpic[] | null = null;
+let lastFetchTime = 0;
+const CACHE_DURATION_4_HOURS = 4 * 60 * 60 * 1000;
+
+export const getGithubRoadmap: GetGithubRoadmap<
+  void,
+  GithubEpic[]
+> = async () => {
+  const currentTime = Date.now();
+
+  if (
+    cachedGithubEpicsData &&
+    currentTime - lastFetchTime < CACHE_DURATION_4_HOURS
+  ) {
+    return cachedGithubEpicsData;
+  } else {
+    lastFetchTime = currentTime;
+  }
+
+  const githubRoadmapToken = process.env.GITHUB_ROADMAP_TOKEN;
+  if (!githubRoadmapToken) {
+    console.warn("GITHUB_ROADMAP_TOKEN is not set");
+    return [];
+  }
+
+  const githubGraphQlQuery = `
+    query($org: String!, $number: Int!) {
+      organization(login: $org) {
+        projectV2(number: $number) {
+          items(first: 100) {
+            nodes {
+              id
+              databaseId
+              fieldValues(first: 50) {
+                nodes {
+                  ... on ProjectV2ItemFieldSingleSelectValue {
+                    name
+                    field { ... on ProjectV2FieldCommon { name } }
+                  }
+                }
+              }
+              content {
+                ... on Issue {
+                  title
+                  url
+                  number
+                  issueType { name }
+                  subIssues(first: 100) {
+                    totalCount
+                    nodes {
+                      state
+                      number
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  `;
+
+  try {
+    const response = await fetch("https://api.github.com/graphql", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: `Bearer ${githubRoadmapToken}`,
+      },
+      body: JSON.stringify({
+        query: githubGraphQlQuery,
+        variables: {
+          org: GITHUB_ORG_NAME,
+          number: OPEN_SAAS_GITHUB_PROJECT_NUMBER,
+        },
+      }),
+    });
+
+    const json = await response.json();
+
+    const parseResult = GithubGraphQLResponseSchema.safeParse(json);
+    if (!parseResult.success) {
+      console.error(
+        "GitHub API response validation failed:",
+        parseResult.error,
+      );
+      return cachedGithubEpicsData || [];
+    }
+
+    const validatedResponse = parseResult.data;
+
+    if (validatedResponse.errors) {
+      console.error("GitHub API Errors:", validatedResponse.errors);
+      throw new Error("GitHub API returned errors");
+    }
+
+    const nodes =
+      validatedResponse.data?.organization?.projectV2?.items?.nodes || [];
+    const statusFieldName = "Status";
+
+    const BASE_PROJECT_URL = `https://github.com/orgs/${GITHUB_ORG_NAME}/projects/${OPEN_SAAS_GITHUB_PROJECT_NUMBER}/views/1?pane=issue&itemId=`;
+
+    const epics: GithubEpic[] = nodes
+      .filter((node) => node.content?.issueType?.name === "Epic")
+      .map((node) => {
+        const issue = node.content;
+
+        const statusField = node.fieldValues.nodes.find(
+          (f) => f.field?.name === statusFieldName,
+        );
+        const statusName = statusField?.name || "Ideas";
+
+        let status: GithubEpicStatus = GithubEpicStatus.Ideas;
+        if (
+          Object.values(GithubEpicStatus).includes(
+            statusName as GithubEpicStatus,
+          )
+        ) {
+          status = statusName as GithubEpicStatus;
+        } else {
+          console.warn(`Unknown GitHub Epic status: ${statusName}`);
+        }
+
+        const subIssues = issue.subIssues?.nodes || [];
+        const totalIssues = issue.subIssues?.totalCount ?? subIssues.length;
+        const doneIssues = subIssues.filter(
+          (sub) => sub.state === "CLOSED",
+        ).length;
+
+        return {
+          name: issue.title,
+          totalIssues,
+          doneIssues,
+          status,
+          url: `${BASE_PROJECT_URL}${node.databaseId}`,
+        };
+      });
+
+    epics.sort((a, b) => getStatusOrder(a.status) - getStatusOrder(b.status));
+
+    cachedGithubEpicsData = epics;
+    return epics;
+  } catch (error) {
+    console.error("Failed to fetch GitHub roadmap:", error);
+    return cachedGithubEpicsData || [];
+  }
+};
+
+const getStatusOrder = (status: GithubEpicStatus): number => {
+  switch (status) {
+    case GithubEpicStatus.InProgress:
+      return 0;
+    case GithubEpicStatus.Planned:
+      return 1;
+    case GithubEpicStatus.Ideas:
+      return 2;
+    case GithubEpicStatus.Done:
+      return 3;
+    default:
+      return assertUnreachable(status);
+  }
+};
+
+const GithubIssueSchema = z.object({
+  title: z.string(),
+  url: z.string(),
+  number: z.number(),
+  issueType: z
+    .object({
+      name: z.string(),
+    })
+    .nullish(),
+  subIssues: z
+    .object({
+      totalCount: z.number(),
+      nodes: z.array(
+        z.object({
+          state: z.string(),
+          number: z.number(),
+        }),
+      ),
+    })
+    .nullish(),
+});
+
+const ProjectV2ItemFieldValueSchema = z.object({
+  name: z.string().nullish(),
+  field: z
+    .object({
+      name: z.string(),
+    })
+    .nullish(),
+});
+
+const ProjectV2ItemSchema = z.object({
+  id: z.string(),
+  databaseId: z.number().nullish(),
+  fieldValues: z.object({
+    nodes: z.array(ProjectV2ItemFieldValueSchema),
+  }),
+  content: GithubIssueSchema,
+});
+
+const GithubGraphQLResponseSchema = z.object({
+  data: z
+    .object({
+      organization: z
+        .object({
+          projectV2: z
+            .object({
+              items: z
+                .object({
+                  nodes: z.array(ProjectV2ItemSchema),
+                })
+                .nullish(),
+            })
+            .nullish(),
+        })
+        .nullish(),
+    })
+    .nullish(),
+  errors: z.array(z.any()).nullish(),
+});
