Repository: PotLock/curatedotfun
Pull request #156: Update feeds
Author: login: elliotBraem
Created: 2025-05-19T01:27:40Z | Merged: 2025-05-19T01:35:50Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added API endpoints for creating, updating, and retrieving individual feeds with validation and error handling.
  - Enhanced feed details available to users, including metadata such as name, description, and timestamps.

- **Bug Fixes**
  - Corrected API endpoint paths throughout the application and tests for consistency and improved RESTful design.

- **Refactor**
  - Improved error responses for bad requests with more detailed feedback.
  - Updated hooks and data fetching logic to align with new API structure and provide richer feed information.

- **Chores**
  - Updated Dockerfile to include the entire schema directory in the production build.
  - Cleaned up comments and import paths for better maintainability.

- **Documentation**
  - Improved validation schemas for feed and configuration data, ensuring stricter and clearer input requirements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/Dockerfile b/Dockerfile
index b2bfbe23..87d1b768 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -48,6 +48,7 @@ COPY --from=builder --chown=app:app /app/backend/package.json ./backend/package.
 COPY --from=builder --chown=app:app /app/backend/drizzle.config.ts ./backend/drizzle.config.ts
 COPY --from=builder --chown=app:app /app/backend/src/services/db/migrations ./backend/src/services/db/migrations
 COPY --from=builder --chown=app:app /app/backend/src/services/db/schema.ts ./backend/src/services/db/schema.ts
+COPY --from=builder --chown=app:app /app/backend/src/services/db/schema ./backend/src/services/db/schema
 COPY --from=builder --chown=app:app /app/package.json ./
 COPY --from=builder --chown=app:app /app/pnpm-lock.yaml ./
 COPY --from=builder --chown=app:app /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
diff --git a/backend/src/routes/api/feed.ts b/backend/src/routes/api/feeds.ts
similarity index 51%
rename from backend/src/routes/api/feed.ts
rename to backend/src/routes/api/feeds.ts
index 7908e8f6..e70d258d 100644
--- a/backend/src/routes/api/feed.ts
+++ b/backend/src/routes/api/feeds.ts
@@ -1,48 +1,132 @@
 import { Hono } from "hono";
 import { Env } from "../../types/app";
 import { feedRepository } from "../../services/db/repositories";
-import { serviceUnavailable } from "../../utils/error";
+import { serviceUnavailable, badRequest } from "../../utils/error";
 import { logger } from "../../utils/logger";
 import { DistributorConfig, StreamConfig } from "../../types/config";
+import {
+  insertFeedSchema,
+  updateFeedSchema,
+} from "../../validation/feed.validation";
 
-const feedRoutes = new Hono<Env>();
+const feedsRoutes = new Hono<Env>();
 
 /**
  * Get all feeds
  */
-feedRoutes.get("/", async (c) => {
-  const context = c.get("context");
-  return c.json(context.configService.getConfig().feeds);
+feedsRoutes.get("/", async (c) => {
+  try {
+    const feeds = await feedRepository.getAllFeeds();
+    return c.json(feeds);
+  } catch (error) {
+    logger.error("Error fetching all feeds:", error);
+    return c.json({ error: "Failed to fetch feeds" }, 500);
+  }
+});
+
+/**
+ * Create a new feed
+ */
+feedsRoutes.post("/", async (c) => {
+  const body = await c.req.json();
+  const validationResult = insertFeedSchema.safeParse(body);
+
+  if (!validationResult.success) {
+    return badRequest(c, "Invalid feed data", validationResult.error.flatten());
+  }
+
+  try {
+    const newFeed = await feedRepository.createFeed(validationResult.data);
+    return c.json(newFeed, 201);
+  } catch (error) {
+    logger.error("Error creating feed:", error);
+    return c.json({ error: "Failed to create feed" }, 500);
+  }
+});
+
+/**
+ * Get a specific feed by its ID
+ */
+feedsRoutes.get("/:feedId", async (c) => {
+  const feedId = c.req.param("feedId");
+  try {
+    const feed = await feedRepository.getFeedById(feedId);
+    if (!feed) {
+      return c.notFound();
+    }
+    return c.json(feed);
+  } catch (error) {
+    logger.error(`Error fetching feed ${feedId}:`, error);
+    return c.json({ error: "Failed to fetch feed" }, 500);
+  }
 });
 
 /**
  * Get submissions for a specific feed
  */
-feedRoutes.get("/:feedId", async (c) => {
-  const context = c.get("context");
+feedsRoutes.get("/:feedId/submissions", async (c) => {
+  const feedId = c.req.param("feedId");
+  try {
+    // Check if feed exists before fetching submissions
+    const feedExists = await feedRepository.getFeedById(feedId);
+    if (!feedExists) {
+      return c.notFound();
+    }
+    const submissions = await feedRepository.getSubmissionsByFeed(feedId);
+    return c.json(submissions);
+  } catch (error) {
+    logger.error(`Error fetching submissions for feed ${feedId}:`, error);
+    return c.json({ error: "Failed to fetch submissions" }, 500);
+  }
+});
+
+/**
+ * Update an existing feed
+ */
+feedsRoutes.put("/:feedId", async (c) => {
   const feedId = c.req.param("feedId");
+  const body = await c.req.json();
+  const validationResult = updateFeedSchema.safeParse(body);
 
-  const feed = context.configService.getFeedConfig(feedId);
-  if (!feed) {
-    return c.notFound();
+  if (!validationResult.success) {
+    return badRequest(c, "Invalid feed data", validationResult.error.flatten());
   }
 
-  return c.json(await feedRepository.getSubmissionsByFeed(feedId));
+  try {
+    const updatedFeed = await feedRepository.updateFeed(
+      feedId,
+      validationResult.data,
+    ); // Assumes this method exists
+    if (!updatedFeed) {
+      return c.notFound();
+    }
+    return c.json(updatedFeed);
+  } catch (error) {
+    logger.error(`Error updating feed ${feedId}:`, error);
+    return c.json({ error: "Failed to update feed" }, 500);
+  }
 });
 
 /**
  * Process approved submissions for a feed
  * Optional query parameter: distributors - comma-separated list of distributor plugins to use
- * Example: /api/feed/solana/process?distributors=@curatedotfun/rss
+ * Example: /api/feeds/solana/process?distributors=@curatedotfun/rss
  */
-feedRoutes.post("/:feedId/process", async (c) => {
+feedsRoutes.post("/:feedId/process", async (c) => {
   const context = c.get("context");
   const feedId = c.req.param("feedId");
 
-  const feed = context.configService.getFeedConfig(feedId);
-  if (!feed) {
-    return c.notFound();
+  let feed;
+  try {
+    feed = await feedRepository.getFeedById(feedId);
+    if (!feed) {
+      return c.notFound();
+    }
+  } catch (error) {
+    logger.error(`Error fetching feed ${feedId} for processing:`, error);
+    return c.json({ error: "Failed to fetch feed for processing" }, 500);
   }
+  const feedConfig = feed.config; // FeedConfig is nested under 'config' property
 
   // Get approved submissions for this feed
   const submissions = await feedRepository.getSubmissionsByFeed(feedId);
@@ -67,12 +151,12 @@ feedRoutes.post("/:feedId/process", async (c) => {
 
   for (const submission of approvedSubmissions) {
     try {
-      if (!feed.outputs.stream || !feed.outputs.stream.distribute) {
+      if (!feedConfig.outputs.stream || !feedConfig.outputs.stream.distribute) {
         continue;
       }
 
       // Create a copy of the stream config
-      const streamConfig: StreamConfig = { ...feed.outputs.stream };
+      const streamConfig: StreamConfig = { ...feedConfig.outputs.stream };
 
       // If no distributors specified, use all available
       if (!distributorsParam) {
@@ -140,4 +224,4 @@ feedRoutes.post("/:feedId/process", async (c) => {
   });
 });
 
-export { feedRoutes };
+export { feedsRoutes };
diff --git a/backend/src/routes/api/index.ts b/backend/src/routes/api/index.ts
index 0e7cfc13..a2cae72c 100644
--- a/backend/src/routes/api/index.ts
+++ b/backend/src/routes/api/index.ts
@@ -1,7 +1,7 @@
 import { Hono } from "hono";
 import { isProduction } from "../../services/config.service";
 import { configRoutes } from "./config";
-import { feedRoutes } from "./feed";
+import { feedsRoutes } from "./feeds";
 import { leaderboardRoutes } from "./leaderboard";
 import { pluginRoutes } from "./plugin";
 import { statsRoutes } from "./stats";
@@ -24,8 +24,7 @@ if (!isProduction) {
 // Mount sub-routers
 apiRoutes.route("/twitter", twitterRoutes);
 apiRoutes.route("/submissions", submissionRoutes);
-apiRoutes.route("/feed", feedRoutes);
-apiRoutes.route("/feeds", feedRoutes); // Alias for compatibility (TODO: Fix, combine into one)
+apiRoutes.route("/feeds", feedsRoutes);
 apiRoutes.route("/config", configRoutes);
 apiRoutes.route("/plugins", pluginRoutes);
 apiRoutes.route("/leaderboard", leaderboardRoutes);
diff --git a/backend/src/services/db/repositories/feed.repository.ts b/backend/src/services/db/repositories/feed.repository.ts
index d35d9fbb..058695f0 100644
--- a/backend/src/services/db/repositories/feed.repository.ts
+++ b/backend/src/services/db/repositories/feed.repository.ts
@@ -1,6 +1,11 @@
 import { and, desc, eq, sql } from "drizzle-orm";
 import { FeedConfig } from "../../../types/config";
 import { RecapState } from "../../../types/recap";
+import {
+  InsertFeedData,
+  SelectFeedData,
+  UpdateFeedData,
+} from "../../../validation/feed.validation";
 import {
   Submission,
   SubmissionFeed,
@@ -30,6 +35,58 @@ export interface ApprovedSubmission {
  * Repository for feed-related database operations
  */
 export class FeedRepository {
+  /**
+   * Get a feed by ID
+   */
+  async getFeedById(feedId: string): Promise<SelectFeedData | null> {
+    return executeOperation(async (db) => {
+      const result = await db
+        .select()
+        .from(feeds)
+        .where(eq(feeds.id, feedId))
+        .limit(1);
+      return result.length > 0 ? (result[0] as SelectFeedData) : null;
+    });
+  }
+
+  /**
+   * Get all feeds
+   */
+  async getAllFeeds(): Promise<SelectFeedData[]> {
+    return executeOperation(async (db) => {
+      const result = await db.select().from(feeds);
+      return result as SelectFeedData[];
+    });
+  }
+
+  /**
+   * Create a new feed
+   */
+  async createFeed(data: InsertFeedData): Promise<SelectFeedData> {
+    return executeTransaction(async (db) => {
+      const result = await db.insert(feeds).values(data).returning();
+      return result[0] as SelectFeedData;
+    }, true); // isWrite = true
+  }
+
+  /**
+   * Update an existing feed
+   */
+  async updateFeed(
+    feedId: string,
+    data: UpdateFeedData,
+  ): Promise<SelectFeedData | null> {
+    return executeTransaction(async (db) => {
+      const result = await db
+        .update(feeds)
+        .set({ ...data, updatedAt: new Date() })
+        .where(eq(feeds.id, feedId))
+        .returning();
+      return result.length > 0 ? (result[0] as SelectFeedData) : null;
+    }, true); // isWrite = true
+  }
+
+  // --- Existing methods for feed config and recap state ---
   /**
    * Get a feed's configuration by ID
    */
@@ -62,23 +119,6 @@ export class FeedRepository {
     });
   }
 
-  /**
-   * Update a feed's configuration
-   */
-  async updateFeedConfig(feedId: string, config: FeedConfig): Promise<void> {
-    await executeOperation(async (db) => {
-      await db
-        .update(feeds)
-        .set({
-          config,
-          name: config.name,
-          description: config.description,
-          updatedAt: new Date(),
-        })
-        .where(eq(feeds.id, feedId));
-    }, true); // isWrite = true
-  }
-
   /**
    * Get a recap state by feed ID and recap ID
    */
diff --git a/backend/src/services/db/schema.ts b/backend/src/services/db/schema.ts
index 046d9569..cfe19764 100644
--- a/backend/src/services/db/schema.ts
+++ b/backend/src/services/db/schema.ts
@@ -1,4 +1,3 @@
-import { sql } from "drizzle-orm";
 import {
   date,
   index,
@@ -14,8 +13,8 @@ import {
 
 import { FeedConfig } from "../../types/config";
 import { Profile } from "../../types/zod/userProfile";
+import { sql } from "drizzle-orm";
 
-// Import activity schema
 export * from "./schema/activity";
 
 export type Metadata = {
diff --git a/backend/src/services/db/types.ts b/backend/src/services/db/types.ts
deleted file mode 100644
index dec1cf8a..00000000
--- a/backend/src/services/db/types.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { SubmissionStatus } from "../../types/twitter";
-
-export interface DbSubmission {
-  tweetId: string;
-  userId: string;
-  username: string;
-  content: string;
-  curatorNotes: string | null;
-  curatorId: string;
-  curatorUsername: string;
-  curatorTweetId: string;
-  createdAt: Date;
-  submittedAt: string | null;
-}
-
-export interface DbModeration {
-  tweetId: string | null;
-  feedId: string | null;
-  adminId: string | null;
-  action: string | null;
-  note: string | null;
-  createdAt: Date | null;
-  moderationResponseTweetId: string | null;
-}
-
-export interface DbQueryResult {
-  s: DbSubmission;
-  m: DbModeration;
-}
-
-// Type for raw database results
-export interface RawDbQueryResult {
-  s: {
-    tweetId: string;
-    userId: string;
-    username: string;
-    content: string;
-    curatorNotes: string | null;
-    curatorId: string;
-    curatorUsername: string;
-    curatorTweetId: string;
-    createdAt: Date;
-    submittedAt: string;
-  };
-  m: {
-    tweetId: string | null;
-    feedId: string | null;
-    adminId: string | null;
-    action: string | null;
-    note: string | null;
-    createdAt: Date | null;
-    moderationResponseTweetId: string | null;
-  };
-}
-
-export interface DbFeedQueryResult extends DbQueryResult {
-  sf: {
-    status: SubmissionStatus;
-  };
-}
diff --git a/backend/src/types/app.ts b/backend/src/types/app.ts
index e6eac315..c68f2626 100644
--- a/backend/src/types/app.ts
+++ b/backend/src/types/app.ts
@@ -1,8 +1,8 @@
 import { JwtVariables } from "hono/jwt";
-import { ConfigService } from "../services/config/config.service";
+import { ConfigService } from "../services/config.service";
 import { FeedRepository } from "../services/db/repositories/feed.repository";
 import { DistributionService } from "../services/distribution.service";
-import { ProcessorService } from "../services/processor/processor.service";
+import { ProcessorService } from "../services/processor.service";
 import { SubmissionService } from "../services/submission.service";
 import { TwitterService } from "../services/twitter/client";
 import { Hono } from "hono";
diff --git a/backend/src/types/zod/userProfile.ts b/backend/src/types/zod/userProfile.ts
index 75c5d32f..eedb2114 100644
--- a/backend/src/types/zod/userProfile.ts
+++ b/backend/src/types/zod/userProfile.ts
@@ -3,7 +3,6 @@ import { z } from "zod";
 export const socialImageSchema = z
   .object({
     url: z.string().url().optional(),
-    ipfs_cid: z.string().optional(),
   })
   .strict();
 
@@ -17,6 +16,5 @@ export const profileSchema = z
   })
   .strict();
 
-// You can infer TypeScript types from Zod schemas if needed elsewhere
 export type Profile = z.infer<typeof profileSchema>;
 export type SocialImage = z.infer<typeof socialImageSchema>;
diff --git a/backend/src/utils/error.ts b/backend/src/utils/error.ts
index 603363d4..ad25d636 100644
--- a/backend/src/utils/error.ts
+++ b/backend/src/utils/error.ts
@@ -47,3 +47,21 @@ export function serviceUnavailable(service: string): HTTPException {
     message: `${service} service not available`,
   });
 }
+
+/**
+ * Helper to create a bad request error
+ * @param c - Hono context
+ * @param message - Optional custom message
+ * @param details - Optional details about the validation errors
+ */
+export function badRequest(
+  c: Context,
+  message = "Bad Request",
+  details?: unknown,
+) {
+  const responseBody: { error: string; details?: unknown } = { error: message };
+  if (details) {
+    responseBody.details = details;
+  }
+  return c.json(responseBody, ErrorCode.BAD_REQUEST);
+}
diff --git a/backend/src/validation/activity.validation.ts b/backend/src/validation/activity.validation.ts
index 78e4bfe0..a5093fb4 100644
--- a/backend/src/validation/activity.validation.ts
+++ b/backend/src/validation/activity.validation.ts
@@ -13,7 +13,7 @@ export const insertActivitySchema = createInsertSchema(schema.activities, {
   id: z.undefined(),
   createdAt: z.undefined(),
   updatedAt: z.undefined(),
-  timestamp: z.undefined(), // Will be set to current timestamp by default
+  timestamp: z.undefined(),
 });
 
 export const selectActivitySchema = createSelectSchema(schema.activities);
diff --git a/backend/src/validation/config.validation.ts b/backend/src/validation/config.validation.ts
new file mode 100644
index 00000000..5887b5f5
--- /dev/null
+++ b/backend/src/validation/config.validation.ts
@@ -0,0 +1,60 @@
+import { z } from "zod";
+
+// Schema for TransformConfig
+export const transformConfigSchema = z.object({
+  plugin: z.string(),
+  config: z.record(z.string(), z.unknown()),
+});
+
+// Schema for DistributorConfig
+export const distributorConfigSchema = z.object({
+  plugin: z.string(),
+  config: z.record(z.string(), z.string()),
+  transform: z.array(transformConfigSchema).optional(),
+});
+
+// Schema for ModerationConfig
+export const moderationConfigSchema = z.object({
+  approvers: z.object({
+    twitter: z.array(z.string()),
+  }),
+});
+
+// Schema for StreamConfig
+export const streamConfigSchema = z.object({
+  enabled: z.boolean(),
+  transform: z.array(transformConfigSchema).optional(),
+  distribute: z.array(distributorConfigSchema).optional(),
+});
+
+// Schema for RecapConfig
+export const recapConfigSchema = z.object({
+  id: z.string(),
+  name: z.string(),
+  enabled: z.boolean(),
+  schedule: z.string(),
+  timezone: z.string().optional(),
+  transform: z.array(transformConfigSchema).optional(),
+  batchTransform: z.array(transformConfigSchema).optional(),
+  distribute: z.array(distributorConfigSchema).optional(),
+});
+
+// Schema for FeedConfig
+export const feedConfigSchema = z.object({
+  id: z.string().min(1, "Feed ID cannot be empty"), // Assuming id is the hashtag and should not be empty
+  name: z.string().min(1, "Feed name cannot be empty"),
+  description: z.string(), // Description can be empty
+  moderation: moderationConfigSchema,
+  outputs: z.object({
+    stream: streamConfigSchema.optional(),
+    recap: z.array(recapConfigSchema).optional(),
+  }),
+});
+
+// Type exports for convenience
+export type TransformConfigData = z.infer<typeof transformConfigSchema>;
+export type DistributorConfigData = z.infer<typeof distributorConfigSchema>;
+export type ModerationConfigData = z.infer<typeof moderationConfigSchema>;
+export type StreamConfigData = z.infer<typeof streamConfigSchema>;
+export type RecapConfigData = z.infer<typeof recapConfigSchema>;
+export type FeedConfigData = z.infer<typeof feedConfigSchema>;
diff --git a/backend/src/validation/feed.validation.ts b/backend/src/validation/feed.validation.ts
new file mode 100644
index 00000000..a711d514
--- /dev/null
+++ b/backend/src/validation/feed.validation.ts
@@ -0,0 +1,34 @@
+import { z } from "zod";
+import {
+  createInsertSchema,
+  createSelectSchema,
+  createUpdateSchema,
+} from "drizzle-zod";
+import * as schema from "../services/db/schema";
+import { feedConfigSchema } from "./config.validation";
+
+export const insertFeedSchema = createInsertSchema(schema.feeds, {
+  // id is the primary key (text) and should be provided by the user (e.g., hashtag)
+  id: z.string().min(1, "Feed ID (hashtag) cannot be empty"),
+  name: z.string().min(1, "Feed name cannot be empty"),
+  description: z.string().optional(),
+  config: feedConfigSchema,
+  createdAt: z.undefined(),
+  updatedAt: z.undefined(),
+});
+
+export const selectFeedSchema = createSelectSchema(schema.feeds);
+
+export const updateFeedSchema = createUpdateSchema(schema.feeds, {
+  // id should not be updatable (TODO: migrate away from hashtag being id)
+  id: z.undefined(),
+  name: z.string().min(1, "Feed name cannot be empty").optional(),
+  description: z.string().optional(),
+  config: feedConfigSchema.optional(),
+  createdAt: z.undefined(),
+  updatedAt: z.undefined(),
+});
+
+export type InsertFeedData = z.infer<typeof insertFeedSchema>;
+export type SelectFeedData = z.infer<typeof selectFeedSchema>;
+export type UpdateFeedData = z.infer<typeof updateFeedSchema>;
diff --git a/backend/test/component/submission-flow.test.ts b/backend/test/component/submission-flow.test.ts
index af40e8bf..5dacd8e0 100644
--- a/backend/test/component/submission-flow.test.ts
+++ b/backend/test/component/submission-flow.test.ts
@@ -80,7 +80,7 @@ describe("Submission Flow", () => {
 
     // Verify submissions were created for each feed
     for (const feedId of feedIds) {
-      const submissionsResponse = await apiClient.get(`/api/feed/${feedId}`);
+      const submissionsResponse = await apiClient.get(`/api/feeds/${feedId}`);
       expect(submissionsResponse.status).toBe(200);
 
       const submission = submissionsResponse.data.submissions.find(
diff --git a/backend/test/e2e/full-flow.test.ts b/backend/test/e2e/full-flow.test.ts
index f14b04e6..f7907791 100644
--- a/backend/test/e2e/full-flow.test.ts
+++ b/backend/test/e2e/full-flow.test.ts
@@ -154,7 +154,7 @@ describe("Full Flow E2E", () => {
       .reply(200, { success: true });
 
     // Act - Generate recap
-    const recapResponse = await apiClient.post("/api/feed/test-feed/recap", {
+    const recapResponse = await apiClient.post("/api/feeds/test-feed/recap", {
       timeframe: "daily",
     });
 
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index b1f16b03..a2dd37b1 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -5,13 +5,23 @@ import type { AppConfig, FeedConfig } from "../types/config";
 import type { SubmissionWithFeedData } from "../types/twitter";
 import { usernameSchema, UserProfile } from "./validation/user";
 
-export function useFeedConfig(feedId: string) {
-  return useQuery<FeedConfig>({
-    queryKey: ["feed", feedId],
+// TODO: Implement a shared types package
+export interface FeedDetails {
+  id: string;
+  name: string;
+  description: string | null;
+  config: FeedConfig;
+  createdAt: string;
+  updatedAt: string | null;
+}
+
+export function useFeed(feedId: string) {
+  return useQuery<FeedDetails>({
+    queryKey: ["feed-details", feedId],
     queryFn: async () => {
-      const response = await fetch(`/api/config/${feedId}`);
+      const response = await fetch(`/api/feeds/${feedId}`);
       if (!response.ok) {
-        throw new Error("Failed to fetch feed config");
+        throw new Error("Failed to fetch feed details");
       }
       return response.json();
     },
@@ -30,7 +40,7 @@ export function useFeedItems(
     [string, string, string | undefined],
     number
   >({
-    queryKey: ["feed-items-paginated", feedId, status],
+    queryKey: ["feed-submissions-paginated", feedId, status],
     queryFn: async ({ pageParam = 0 }) => {
       const statusParam = status ? `status=${status}` : "";
       const pageParamStr = `page=${pageParam}`;
@@ -41,10 +51,10 @@ export function useFeedItems(
         .filter((param) => param !== "")
         .join("&");
 
-      const url = `/api/feed/${feedId}?${queryParams}`;
+      const url = `/api/feeds/${feedId}/submissions?${queryParams}`;
       const response = await fetch(url);
       if (!response.ok) {
-        throw new Error("Failed to fetch feed items");
+        throw new Error("Failed to fetch feed submissions");
       }
       return response.json();
     },
diff --git a/frontend/src/lib/recap.ts b/frontend/src/lib/recap.ts
index 650440ca..99178c53 100644
--- a/frontend/src/lib/recap.ts
+++ b/frontend/src/lib/recap.ts
@@ -26,7 +26,7 @@ export function useRecaps(feedId: string) {
   return useQuery<GetRecapsResponse>({
     queryKey: ["recaps", feedId],
     queryFn: async () => {
-      const response = await fetch(`/api/feed/${feedId}/recap`);
+      const response = await fetch(`/api/feeds/${feedId}/recap`);
       if (!response.ok) {
         const errorData = await response.json().catch(() => ({}));
         throw new Error(
@@ -45,7 +45,7 @@ export function useRecap(feedId: string, recapIndex: number) {
   return useQuery<GetRecapResponse>({
     queryKey: ["recap", feedId, recapIndex],
     queryFn: async () => {
-      const response = await fetch(`/api/feed/${feedId}/recap/${recapIndex}`);
+      const response = await fetch(`/api/feeds/${feedId}/recap/${recapIndex}`);
       if (!response.ok) {
         const errorData = await response.json().catch(() => ({}));
         throw new Error(
@@ -65,7 +65,7 @@ export function useAddRecap(feedId: string) {
 
   return useMutation({
     mutationFn: async (recapConfig: RecapConfig) => {
-      const response = await fetch(`/api/feed/${feedId}/recap`, {
+      const response = await fetch(`/api/feeds/${feedId}/recap`, {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
@@ -98,7 +98,7 @@ export function useUpdateRecap(feedId: string, recapIndex: number) {
 
   return useMutation({
     mutationFn: async (recapConfig: RecapConfig) => {
-      const response = await fetch(`/api/feed/${feedId}/recap/${recapIndex}`, {
+      const response = await fetch(`/api/feeds/${feedId}/recap/${recapIndex}`, {
         method: "PUT",
         headers: {
           "Content-Type": "application/json",
@@ -137,7 +137,7 @@ export function useDeleteRecap(feedId: string, recapIndex: number) {
 
   return useMutation({
     mutationFn: async () => {
-      const response = await fetch(`/api/feed/${feedId}/recap/${recapIndex}`, {
+      const response = await fetch(`/api/feeds/${feedId}/recap/${recapIndex}`, {
         method: "DELETE",
       });
 
@@ -169,7 +169,7 @@ export function useRunRecap(feedId: string, recapIndex: number) {
   return useMutation({
     mutationFn: async () => {
       const response = await fetch(
-        `/api/feed/${feedId}/recap/${recapIndex}/run`,
+        `/api/feeds/${feedId}/recap/${recapIndex}/run`,
         {
           method: "POST",
         },
diff --git a/frontend/src/routes/_layout/feed/$feedId.tsx b/frontend/src/routes/_layout/feed/$feedId.tsx
index b79ef1fe..66531e76 100644
--- a/frontend/src/routes/_layout/feed/$feedId.tsx
+++ b/frontend/src/routes/_layout/feed/$feedId.tsx
@@ -10,7 +10,7 @@ import {
 } from "lucide-react";
 import FeedLayout from "../../../components/FeedLayout";
 import { Badge } from "../../../components/ui/badge";
-import { useFeedConfig } from "../../../lib/api";
+import { useFeed } from "../../../lib/api";
 
 const TABS = [
   {
@@ -56,7 +56,7 @@ export const Route = createFileRoute("/_layout/feed/$feedId")({
 
 function FeedPageLayout() {
   const { feedId } = Route.useParams();
-  const { data: feed } = useFeedConfig(feedId);
+  const { data: feed } = useFeed(feedId);
 
   return (
     <div className="max-w-[1440px] mx-auto w-full">


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>backend/test/e2e/full-flow.test.ts (1)</summary><blockquote>

`132-132`: _⚠️ Potential issue_

**Inconsistent API path format in mock.**

While the main endpoint was updated to use `/api/feeds/`, this mock is still using the old `/api/feed/` path format.

```diff
- .get(/\/api\/feed\/.*\/moderators/)
+ .get(/\/api\/feeds\/.*\/moderators/)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>backend/src/validation/activity.validation.ts (1)</summary><blockquote>

`16-16`: **Consider adding back a descriptive comment.**

A comment was removed that likely explained how this field is handled. If `timestamp` is still set to a default value elsewhere, consider adding a clarifying comment back to maintain code documentation quality.

```diff
-  timestamp: z.undefined(),
+  timestamp: z.undefined(), // Will be set to current timestamp by default
```

</blockquote></details>
<details>
<summary>backend/src/services/db/repositories/feed.repository.ts (1)</summary><blockquote>

`52-60`: **Consider adding pagination for getAllFeeds.**

While the implementation is correct, consider adding pagination support for larger datasets, especially if the number of feeds is expected to grow.


```diff
async getAllFeeds(): Promise<SelectFeedData[]> {
  return executeOperation(async (db) => {
    const result = await db.select().from(feeds);
    return result as SelectFeedData[];
  });
}

+ /**
+  * Get paginated feeds
+  */
+ async getPaginatedFeeds(page: number = 0, limit: number = 20): Promise<{
+   items: SelectFeedData[];
+   total: number;
+ }> {
+   return executeOperation(async (db) => {
+     const offset = page * limit;
+     const [items, countResult] = await Promise.all([
+       db.select().from(feeds).limit(limit).offset(offset),
+       db.select({ count: sql`count(*)` }).from(feeds)
+     ]);
+     
+     return {
+       items: items as SelectFeedData[],
+       total: Number(countResult[0].count)
+     };
+   });
+ }
```

</blockquote></details>
<details>
<summary>backend/src/routes/api/feeds.ts (1)</summary><blockquote>

`149-160`: **Shallow copy still aliases nested objects – clone `distribute` if you mutate it**

`{ ...feedConfig.outputs.stream }` creates a new wrapper but the inner `distribute` array is *by reference*. When you later replace it with a filtered array (`streamConfig.distribute = …`) you are safe, but if future logic does an in-place mutation (`push`, `splice`, etc.) it will leak into the persisted config.

Defensive copy keeps things future-proof:

```diff
-const streamConfig: StreamConfig = { ...feedConfig.outputs.stream };
+const streamConfig: StreamConfig = {
+  ...feedConfig.outputs.stream,
+  distribute: feedConfig.outputs.stream?.distribute
+    ? [...feedConfig.outputs.stream.distribute]
+    : undefined,
+};
```

</blockquote></details>
<details>
<summary>backend/src/validation/config.validation.ts (1)</summary><blockquote>

`44-47`: **`description` is required but documented as optional – clarify intent**

The comment says *“Description can be empty”* yet the field is mandatory (`z.string()`).  
If the field should truly be optional, consider:

```diff
-description: z.string(),
+description: z.string().optional().default(""),
```

or tighten validation with `z.string().min(1)` if you want to forbid empty strings. Aligning docs and schema reduces confusion for API consumers.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 19c491ac854715d47fbc9c3393c3835af4086dee and a22c57f965443ef7be91825b534679eabec671db.

</details>

<details>
<summary>📒 Files selected for processing (17)</summary>

* `Dockerfile` (1 hunks)
* `backend/src/routes/api/feeds.ts` (3 hunks)
* `backend/src/routes/api/index.ts` (2 hunks)
* `backend/src/services/db/repositories/feed.repository.ts` (2 hunks)
* `backend/src/services/db/schema.ts` (1 hunks)
* `backend/src/services/db/types.ts` (0 hunks)
* `backend/src/types/app.ts` (1 hunks)
* `backend/src/types/zod/userProfile.ts` (0 hunks)
* `backend/src/utils/error.ts` (1 hunks)
* `backend/src/validation/activity.validation.ts` (1 hunks)
* `backend/src/validation/config.validation.ts` (1 hunks)
* `backend/src/validation/feed.validation.ts` (1 hunks)
* `backend/test/component/submission-flow.test.ts` (1 hunks)
* `backend/test/e2e/full-flow.test.ts` (1 hunks)
* `frontend/src/lib/api.ts` (3 hunks)
* `frontend/src/lib/recap.ts` (6 hunks)
* `frontend/src/routes/_layout/feed/$feedId.tsx` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* backend/src/types/zod/userProfile.ts
* backend/src/services/db/types.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (5)</summary>

<details>
<summary>frontend/src/routes/_layout/feed/$feedId.tsx (1)</summary><blockquote>

<details>
<summary>frontend/src/lib/api.ts (1)</summary>

* `useFeed` (18-29)

</details>

</blockquote></details>
<details>
<summary>backend/src/validation/feed.validation.ts (1)</summary><blockquote>

<details>
<summary>backend/src/validation/config.validation.ts (1)</summary>

* `feedConfigSchema` (43-52)

</details>

</blockquote></details>
<details>
<summary>frontend/src/lib/api.ts (1)</summary><blockquote>

<details>
<summary>backend/src/types/config.ts (1)</summary>

* `FeedConfig` (43-52)

</details>

</blockquote></details>
<details>
<summary>backend/src/routes/api/feeds.ts (7)</summary><blockquote>

<details>
<summary>backend/src/types/app.ts (1)</summary>

* `Env` (20-25)

</details>
<details>
<summary>backend/src/services/db/schema.ts (1)</summary>

* `feeds` (43-51)

</details>
<details>
<summary>backend/src/services/db/repositories/feed.repository.ts (1)</summary>

* `feedRepository` (479-479)

</details>
<details>
<summary>backend/src/utils/logger.ts (1)</summary>

* `logger` (71-80)

</details>
<details>
<summary>backend/src/validation/feed.validation.ts (2)</summary>

* `insertFeedSchema` (10-18)
* `updateFeedSchema` (22-30)

</details>
<details>
<summary>backend/src/utils/error.ts (1)</summary>

* `badRequest` (57-67)

</details>
<details>
<summary>backend/src/types/config.ts (1)</summary>

* `StreamConfig` (33-37)

</details>

</blockquote></details>
<details>
<summary>backend/src/services/db/repositories/feed.repository.ts (4)</summary><blockquote>

<details>
<summary>backend/src/validation/feed.validation.ts (3)</summary>

* `SelectFeedData` (33-33)
* `InsertFeedData` (32-32)
* `UpdateFeedData` (34-34)

</details>
<details>
<summary>backend/src/services/db/transaction.ts (2)</summary>

* `executeOperation` (14-22)
* `executeTransaction` (29-54)

</details>
<details>
<summary>backend/src/services/db/index.ts (2)</summary>

* `executeOperation` (4-4)
* `executeTransaction` (5-5)

</details>
<details>
<summary>backend/src/services/db/schema.ts (1)</summary>

* `feeds` (43-51)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: Tests</summary>

<details>
<summary>backend/test/e2e/full-flow.test.ts</summary>

[error] 1-1: Required environment variable TELEGRAM_BOT_TOKEN is not set

---

[error] 1-1: Required environment variable OPENROUTER_API_KEY is not set

---

[error] 78-78: Test assertion failed or test did not complete successfully, causing 'pnpm run test' to exit with code 1

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>backend/src/services/db/schema.ts (1)</summary>

`16-16`: **Import order looks consistent.**

The `sql` import from "drizzle-orm" has been moved to follow local type imports, which is generally consistent with good import ordering practices. This import is used on line 202 for the metadata type index.

</details>
<details>
<summary>backend/src/types/app.ts (1)</summary>

`2-2`: **Import path simplification looks good.**

The import paths for ConfigService and ProcessorService have been simplified by removing intermediate subdirectories. This change aligns with the broader backend refactoring where feed-related functionality was modularized.



Also applies to: 5-5

</details>
<details>
<summary>frontend/src/routes/_layout/feed/$feedId.tsx (2)</summary>

`13-13`: **Import updated to align with API restructuring.**

The change from `useFeedConfig` to `useFeed` aligns with the broader API restructuring that provides enhanced CRUD capabilities for feeds.

---

`59-59`: **Feed data fetching updated to use the new API endpoint.**

The implementation correctly updates the data fetching call to use the new `useFeed` hook, maintaining consistency with the backend API changes.

</details>
<details>
<summary>frontend/src/lib/recap.ts (1)</summary>

`29-29`: **API endpoints updated to use consistent `/api/feeds/` path.**

All recap-related API endpoints have been properly updated from `/api/feed/` to `/api/feeds/` to align with the backend route refactoring. This ensures consistency across the application.



Also applies to: 48-48, 68-68, 101-101, 140-140, 172-172

</details>
<details>
<summary>backend/test/e2e/full-flow.test.ts (1)</summary>

`157-157`: **API endpoint updated in test to match new feeds route structure.**

The test now correctly uses the updated `/api/feeds/test-feed/recap` endpoint for the recap generation step.

</details>
<details>
<summary>Dockerfile (1)</summary>

`51-51`: **Added copy of schema directory to support feed-related database changes.**

The addition of copying the entire schema directory ensures that all schema definitions needed for the enhanced feed API routes are available in the production image. This is important for the proper functioning of the updated feed repository methods.

</details>
<details>
<summary>backend/src/routes/api/index.ts (1)</summary>

`4-4`: **API route consolidation looks good.**

The changes to import `feedsRoutes` from "./feeds" and consolidate the API routes to use a single `/feeds` endpoint follow RESTful API best practices with consistent plural resource naming.



Also applies to: 27-27

</details>
<details>
<summary>backend/src/utils/error.ts (1)</summary>

`51-67`: **Well-structured error handling helper.**

The new `badRequest` helper function provides a standardized way to create error responses, which will improve consistency across the API. The implementation correctly handles optional details and uses the existing error code enum.


One minor suggestion for improved flexibility:

```diff
export function badRequest(
  c: Context,
  message = "Bad Request",
  details?: unknown,
) {
-  const responseBody: { error: string; details?: unknown } = { error: message };
+  const responseBody: Record<string, unknown> = { error: message };
  if (details) {
    responseBody.details = details;
  }
  return c.json(responseBody, ErrorCode.BAD_REQUEST);
}
```

This change would make it easier to extend the response with additional fields in the future if needed.

</details>
<details>
<summary>frontend/src/lib/api.ts (3)</summary>

`9-16`: **Well-structured interface for feed details.**

The `FeedDetails` interface provides a clear data structure for feed information, including metadata and configuration. This aligns well with the backend data model and improves type safety.

---

`18-29`: **Clean implementation of feed fetching hook.**

The new `useFeed` hook follows React Query best practices and properly handles error cases. The endpoint path matches the updated backend routes.

---

`43-43`: **Consistent updates to feed submissions query.**

The changes to query key and API endpoint path maintain consistency with the backend changes. The updated error message is more specific and helpful.



Also applies to: 54-54, 57-57

</details>
<details>
<summary>backend/src/services/db/repositories/feed.repository.ts (5)</summary>

`4-8`: **Good use of typed data interfaces.**

Importing strongly-typed interfaces from validation schemas ensures type safety and consistency between validation and data access layers.

---

`39-50`: **Well-implemented feed retrieval method.**

The `getFeedById` method properly handles the database operation and null case when no feed is found. The use of `executeOperation` follows the established pattern in the repository.

---

`62-70`: **Solid implementation of feed creation.**

The `createFeed` method correctly uses a transaction for write operations and returns the newly created feed with proper typing.

---

`72-87`: **Comprehensive feed update implementation.**

The method handles updating the feed data along with the timestamp and properly returns null when the feed doesn't exist.

---

`89-89`: **Great organization with clear section comment.**

Using a comment to separate new methods from existing ones improves code readability and helps developers understand the file structure.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T01:31:08Z

Review comments:
diff_hunk: @@ -80,7 +80,7 @@ describe("Submission Flow", () => {
 
     // Verify submissions were created for each feed
     for (const feedId of feedIds) {
-      const submissionsResponse = await apiClient.get(`/api/feed/${feedId}`);
+      const submissionsResponse = await apiClient.get(`/api/feeds/${feedId}`);
path: backend/test/component/submission-flow.test.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**API endpoint update looks correct.**

The endpoint has been updated from `/api/feed/${feedId}` to `/api/feeds/${feedId}` to match the consolidated feed endpoints now under the pluralized `/api/feeds` path.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any remaining references to the old /api/feed/ endpoint that might need updating
rg -i "/api/feed/" --type ts --no-filename
```

Length of output: 303

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the files and line numbers containing commented references to the old /api/feed/ endpoint
rg -n "/api/feed/" --type ts
```

Length of output: 503

---



</details>

**Update example endpoint comments to use `/api/feeds`**  

The example API routes in `backend/src/routes/api/recap.ts` still reference the old singular `/api/feed` path in comments. Please update these to `/api/feeds` to keep them in sync with the actual routes:

• backend/src/routes/api/recap.ts:62  
• backend/src/routes/api/recap.ts:99  
• backend/src/routes/api/recap.ts:145  
• backend/src/routes/api/recap.ts:186  
• backend/src/routes/api/recap.ts:237  
• backend/src/routes/api/recap.ts:281  

Replace lines like:  
```ts
// GET /api/feed/:feedId/recap
```  
with:  
```ts
// GET /api/feeds/:feedId/recap
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In backend/src/routes/api/recap.ts at lines 62, 99, 145, 186, 237, and 281,
update the example API route comments from the old singular path `/api/feed` to
the new pluralized path `/api/feeds`. Replace all instances of comments like `//
GET /api/feed/:feedId/recap` with `// GET /api/feeds/:feedId/recap` to keep the
comments consistent with the actual updated routes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T01:31:07Z
author_association: NONE
diff_hunk: @@ -0,0 +1,34 @@
+import { z } from "zod";
+import {
+  createInsertSchema,
+  createSelectSchema,
+  createUpdateSchema,
+} from "drizzle-zod";
+import * as schema from "../services/db/schema";
+import { feedConfigSchema } from "./config.validation";
+
+export const insertFeedSchema = createInsertSchema(schema.feeds, {
+  // id is the primary key (text) and should be provided by the user (e.g., hashtag)
+  id: z.string().min(1, "Feed ID (hashtag) cannot be empty"),
+  name: z.string().min(1, "Feed name cannot be empty"),
+  description: z.string().optional(),
+  config: feedConfigSchema,
+  createdAt: z.undefined(),
+  updatedAt: z.undefined(),
+});
path: backend/src/validation/feed.validation.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider cross-field refinement to keep `id`, `name`, `description` in sync with `config`**  

`insertFeedSchema` & `updateFeedSchema` validate the top-level columns but the nested `config` object already carries its own `id`, `name`, and `description`. When callers are free to supply contradictory values the DB row and the embedded JSONB can silently diverge, making future reads inconsistent and harder to reason about.

A lightweight refinement step can guarantee alignment at write-time:

```diff
 export const insertFeedSchema = createInsertSchema(schema.feeds, {
   …
 }).refine(
   (data) =>
-    data.config.id && data.config.id === data.id &&
-    data.config.name === data.name &&
-    (data.description === undefined || data.config.description === data.description),
+    data.config.id === data.id &&
+    data.config.name === data.name &&
+    (data.description === undefined ||
+      data.config.description === data.description),
   {
     message:
       "Top-level id/name/description must match the values inside config",
     path: ["config"],
   },
 );
```

You can apply the same `refine` to `updateFeedSchema` (guarding only the supplied fields).  
This prevents subtle data mismatches without affecting existing type inference.



Also applies to: 22-30

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In backend/src/validation/feed.validation.ts around lines 10 to 18, the
insertFeedSchema validates top-level fields id, name, and description separately
from the nested config object which also contains these fields, risking data
inconsistency. To fix this, add a schema refinement step that cross-checks and
ensures the id, name, and description at the top level match those inside the
config object, rejecting inputs where they differ. Apply a similar refinement to
updateFeedSchema for the fields provided, ensuring alignment without breaking
existing type inference.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T01:31:07Z
author_association: NONE
diff_hunk: @@ -1,48 +1,132 @@
 import { Hono } from "hono";
 import { Env } from "../../types/app";
 import { feedRepository } from "../../services/db/repositories";
-import { serviceUnavailable } from "../../utils/error";
+import { serviceUnavailable, badRequest } from "../../utils/error";
 import { logger } from "../../utils/logger";
 import { DistributorConfig, StreamConfig } from "../../types/config";
+import {
+  insertFeedSchema,
+  updateFeedSchema,
+} from "../../validation/feed.validation";
 
-const feedRoutes = new Hono<Env>();
+const feedsRoutes = new Hono<Env>();
 
 /**
  * Get all feeds
  */
-feedRoutes.get("/", async (c) => {
-  const context = c.get("context");
-  return c.json(context.configService.getConfig().feeds);
+feedsRoutes.get("/", async (c) => {
+  try {
+    const feeds = await feedRepository.getAllFeeds();
+    return c.json(feeds);
+  } catch (error) {
+    logger.error("Error fetching all feeds:", error);
+    return c.json({ error: "Failed to fetch feeds" }, 500);
+  }
+});
+
+/**
+ * Create a new feed
+ */
+feedsRoutes.post("/", async (c) => {
+  const body = await c.req.json();
+  const validationResult = insertFeedSchema.safeParse(body);
+
+  if (!validationResult.success) {
+    return badRequest(c, "Invalid feed data", validationResult.error.flatten());
+  }
+
path: backend/src/routes/api/feeds.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Parse-error guard: wrap `c.req.json()` in a try/catch**  

`await c.req.json()` throws on malformed JSON or an empty body. At the moment that exception bubbles up and returns a 500 instead of a 400, bypassing your validation branch altogether.

```diff
-const body = await c.req.json();
+let body: unknown;
+try {
+  body = await c.req.json();
+} catch {
+  return badRequest(c, "Request body is not valid JSON");
+}
```

Apply the same guard in the `PUT` handler to keep error semantics consistent across endpoints.



Also applies to: 88-93

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In backend/src/routes/api/feeds.ts around lines 30 to 37 and also lines 88 to
93, the calls to await c.req.json() are not wrapped in try/catch blocks, causing
malformed or empty JSON bodies to throw exceptions that result in 500 errors
instead of returning a 400 bad request. Wrap the await c.req.json() calls in
try/catch blocks to catch parsing errors and return a 400 response with an
appropriate error message, ensuring consistent error handling in both POST and
PUT handlers.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T01:31:07Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-05-19T01:27:44Z
body: [vc]: #v06Csw3XFpc2mHBJTK7NjyfBtfop2slWpCfDiX9sXt8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjdXJhdGVkb3RmdW4tYXBwIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImN1cmF0ZWRvdGZ1bi1hcHAtZ2l0LWZlYXQtdXBkYXRlLWZlZWQtcG90bG9jay52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9wb3Rsb2NrL2N1cmF0ZWRvdGZ1bi1hcHAvQnRBZURQOW5jTUFIWG1LUEJkUWZQVFR6a3ZtMyIsInByZXZpZXdVcmwiOiJjdXJhdGVkb3RmdW4tYXBwLWdpdC1mZWF0LXVwZGF0ZS1mZWVkLXBvdGxvY2sudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **curatedotfun-app** | ✅ Ready ([Inspect](https://vercel.com/potlock/curatedotfun-app/BtAeDP9ncMAHXmKPBdQfPTTzkvm3)) | [Visit Preview](https://curatedotfun-app-git-feat-update-feed-potlock.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/curatedotfun-app-git-feat-update-feed-potlock.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 1:27am |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-19T01:27:46Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces comprehensive improvements to feed management across the backend and frontend. The backend now supports full CRUD operations for feeds with new validation schemas and endpoints, while the frontend adapts to new API routes and returns richer feed details. Several internal types and endpoints are updated or consolidated for consistency and clarity.

## Changes

| File(s)                                                                                  | Change Summary |
|------------------------------------------------------------------------------------------|---------------|
| Dockerfile                                                                               | Production image now copies the entire `/app/backend/src/services/db/schema` directory instead of just `schema.ts`. |
| backend/src/routes/api/feeds.ts                                                          | Refactored and extended feed API routes: renamed handler, switched to DB-backed endpoints, added POST, GET by ID, PUT, and improved error handling. |
| backend/src/routes/api/index.ts                                                          | Updated to import and mount only `feedsRoutes` at `/feeds`, removing `/feed` alias and consolidating feed routes. |
| backend/src/services/db/repositories/feed.repository.ts                                  | Added CRUD methods for feeds (`getFeedById`, `getAllFeeds`, `createFeed`, `updateFeed`), removed `updateFeedConfig`, and enforced typed data interfaces. |
| backend/src/services/db/schema.ts                                                        | Minor: moved `sql` import, removed a comment; no functional changes. |
| backend/src/services/db/types.ts                                                         | Deleted file: removed all DB-related TypeScript interfaces for submissions and moderation. |
| backend/src/types/app.ts                                                                 | Updated import paths for `ConfigService` and `ProcessorService`. |
| backend/src/types/zod/userProfile.ts                                                     | Removed optional `ipfs_cid` field from `socialImageSchema`. |
| backend/src/utils/error.ts                                                               | Added `badRequest` helper function for standardized 400 error responses. |
| backend/src/validation/activity.validation.ts                                            | Removed a comment from the `insertActivitySchema` definition. |
| backend/src/validation/config.validation.ts                                              | New: Added Zod schemas and types for feed configuration objects (transform, distributor, moderation, stream, recap, feed config). |
| backend/src/validation/feed.validation.ts                                                | New: Added Zod schemas and types for feed creation, selection, and update validation. |
| backend/test/component/submission-flow.test.ts                                           | Updated test endpoint from `/api/feed/${feedId}` to `/api/feeds/${feedId}`. |
| backend/test/e2e/full-flow.test.ts                                                       | Updated test endpoint from `/api/feed/test-feed/recap` to `/api/feeds/test-feed/recap`. |
| frontend/src/lib/api.ts                                                                  | Replaced `useFeedConfig` with `useFeed` (returns richer feed details), updated endpoints and query keys for feed and submissions, adjusted error messages. |
| frontend/src/lib/recap.ts                                                                | Updated all recap-related API endpoints from `/api/feed/` to `/api/feeds/`. |
| frontend/src/routes/_layout/feed/$feedId.tsx                                             | Switched from `useFeedConfig` to `useFeed` for fetching feed data in the layout component. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Frontend
    participant Backend API
    participant FeedRepository
    participant Database

    Client->>Frontend: Request feed details (useFeed)
    Frontend->>Backend API: GET /api/feeds/:feedId
    Backend API->>FeedRepository: getFeedById(feedId)
    FeedRepository->>Database: Query feed by ID
    Database-->>FeedRepository: Feed data
    FeedRepository-->>Backend API: Feed details
    Backend API-->>Frontend: Feed details
    Frontend-->>Client: Render feed page with details
```

```mermaid
sequenceDiagram
    participant Client
    participant Frontend
    participant Backend API
    participant FeedRepository
    participant Database

    Client->>Frontend: Submit new feed (useFeed)
    Frontend->>Backend API: POST /api/feeds
    Backend API->>FeedRepository: createFeed(data)
    FeedRepository->>Database: Insert feed
    Database-->>FeedRepository: New feed data
    FeedRepository-->>Backend API: Created feed details
    Backend API-->>Frontend: Feed created response
    Frontend-->>Client: Update UI
```

## Poem

> A rabbit hopped through fields of code,  
> Where feeds now bloom, their details showed.  
> With endpoints fresh and schemas neat,  
> CRUD and queries are a treat!  
> From backend roots to frontend view,  
> This garden grows both strong and new.  
> 🥕✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PotLock/curatedotfun&utm_content=156):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: elliotBraem
created_at: 2025-05-19T01:28:00Z
body: @coderabbitai review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-19T01:28:05Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE