{"id":25407128,"url":"https://github.com/aaronksaunders/payload-chat","last_synced_at":"2025-09-08T00:34:44.514Z","repository":{"id":270415192,"uuid":"910310866","full_name":"aaronksaunders/payload-chat","owner":"aaronksaunders","description":"Payload on the backend with a custom endpoint using (Server-Sent Events) SSE to send updates to the client, The client listens for updates using the EventSource API.","archived":false,"fork":false,"pushed_at":"2024-12-31T01:17:34.000Z","size":182,"stargazers_count":15,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-13T04:53:25.610Z","etag":null,"topics":["nextjs","payload","payload-cms","payloadcms","server-sent-events","sse","sse-client"],"latest_commit_sha":null,"homepage":"https://youtu.be/9ll-8KkRWjo","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aaronksaunders.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-31T00:23:01.000Z","updated_at":"2025-04-04T10:14:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"95896da5-96a4-4c78-9dfb-96ca9f4c8f62","html_url":"https://github.com/aaronksaunders/payload-chat","commit_stats":null,"previous_names":["aaronksaunders/payload-chat"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronksaunders%2Fpayload-chat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronksaunders%2Fpayload-chat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronksaunders%2Fpayload-chat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronksaunders%2Fpayload-chat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aaronksaunders","download_url":"https://codeload.github.com/aaronksaunders/payload-chat/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248665766,"owners_count":21142123,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nextjs","payload","payload-cms","payloadcms","server-sent-events","sse","sse-client"],"created_at":"2025-02-16T06:26:15.949Z","updated_at":"2025-04-13T04:53:29.852Z","avatar_url":"https://github.com/aaronksaunders.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Payload Chat\n\nPayload on the backend with a custom endpoint using [(Server-Sent Events) SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) to send updates to the client, The client listens for updates using the EventSource API.\n\nThis was just an exercise to see what interesting things can be done when using Payload as a platform for building applications, not just as a CMS\n\n## Project Structure\n\n- `client/` - React frontend built with Vite\n- `server/` - Payload backend Server\n\n## Main Components of Solution\n\n### Server\n\nMessages Collection\n\n```javascript\nexport const Messages: CollectionConfig = {\n  slug: \"messages\",\n  // not focused on access in this example\n  access: {\n    create: () =\u003e true,\n    read: () =\u003e true,\n    update: () =\u003e true,\n    delete: () =\u003e true,\n  },\n  endpoints: [SSEMessages], // \u003c-- ADDED CUSTOM ENDPOINT api/messages/sse\n  fields: [\n    {\n      name: \"sender\",\n      type: \"relationship\",\n      relationTo: \"users\",\n      required: true,\n    },\n    {\n      name: \"receiver\",\n      type: \"relationship\",\n      relationTo: \"users\",\n      required: true,\n    },\n    {\n      name: \"content\",\n      type: \"text\",\n      required: true,\n    },\n    {\n      name: \"timestamp\",\n      type: \"date\",\n      required: true,\n      defaultValue: () =\u003e new Date().toISOString(),\n    },\n  ],\n};\n```\n\nCustom Endpoint for SSE Added to Collection\n\n```javascript\nimport type { Endpoint } from \"payload\";\n\n/**\n * Server-Sent Events (SSE) endpoint for Messages collection using TransformStream\n * Implements a polling mechanism to check for new messages and stream them to clients\n */\nexport const SSEMessages: Endpoint = {\n  path: \"/sse\",\n  method: \"get\",\n  handler: async (req) =\u003e {\n    try {\n      // Create abort controller to handle connection termination\n      const abortController = new AbortController();\n      const { signal } = abortController;\n\n      // Set up streaming infrastructure\n      const stream = new TransformStream();\n      const writer = stream.writable.getWriter();\n      const encoder = new TextEncoder();\n\n      // Initialize timestamp to fetch all messages from the beginning\n      let lastTimestamp = new Date(0).toISOString();\n\n      // Send keep-alive messages every 30 seconds to maintain connection\n      const keepAlive = setInterval(async () =\u003e {\n        if (!signal.aborted) {\n          await writer.write(\n            encoder.encode(\"event: ping\\ndata: keep-alive\\n\\n\")\n          );\n        }\n      }, 30000);\n\n      /**\n       * Polls for new messages and sends them to connected clients\n       * - Queries messages newer than the last received message\n       * - Updates lastTimestamp to the newest message's timestamp\n       * - Streams messages to client using SSE format\n       */\n      const pollMessages = async () =\u003e {\n        if (!signal.aborted) {\n          // Query for new messages since last update\n          const messages = await req.payload.find({\n            collection: \"messages\",\n            where: {\n              updatedAt: { greater_than: lastTimestamp },\n            },\n            sort: \"-updatedAt\",\n            limit: 10,\n            depth: 1,\n            populate: {\n              users: {\n                email: true,\n              },\n            },\n          });\n\n          if (messages.docs.length \u003e 0) {\n            // Update timestamp to latest message for next poll\n            lastTimestamp = messages.docs[0].updatedAt;\n            // Send messages to client in SSE format\n            await writer.write(\n              encoder.encode(\n                `event: message\\ndata: ${JSON.stringify(messages.docs)}\\n\\n`\n              )\n            );\n          }\n        }\n      };\n\n      // Poll for new messages every second\n      const messageInterval = setInterval(pollMessages, 1000);\n\n      // Clean up intervals and close writer when connection is aborted\n      signal.addEventListener(\"abort\", () =\u003e {\n        clearInterval(keepAlive);\n        clearInterval(messageInterval);\n        writer.close();\n      });\n\n      // Return SSE response with appropriate headers\n      return new Response(stream.readable, {\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n          \"X-Accel-Buffering\": \"no\", // Prevents nginx from buffering the response\n          \"Access-Control-Allow-Origin\": \"*\", // CORS header for cross-origin requests\n          \"Access-Control-Allow-Methods\": \"GET, OPTIONS\",\n          \"Access-Control-Allow-Headers\": \"Content-Type\",\n        },\n      });\n    } catch (error) {\n      console.log(error);\n      return new Response(\"Error occurred\", { status: 500 });\n    }\n  },\n};\n```\n\n### Client\n\nHow we connect to the server for the SSE\n\n```javascript\nuseEffect(() =\u003e {\n  // Create EventSource connection\n  const eventSource = new EventSource(\n    `${import.meta.env.VITE_API_URL}${endpoint}`\n  );\n\n  // Handle incoming messages\n  eventSource.onmessage = (event) =\u003e {\n    const data = JSON.parse(event.data);\n    setMessages((prev) =\u003e [...prev, data]);\n  };\n\n  // Handle connection open\n  eventSource.onopen = () =\u003e {\n    console.log(\"SSE Connection Established\");\n  };\n\n  // Handle errors\n  eventSource.onerror = (error) =\u003e {\n    console.error(\"SSE Error:\", error);\n    eventSource.close();\n  };\n\n  // Cleanup on component unmount\n  return () =\u003e {\n    eventSource.close();\n  };\n}, [endpoint]);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronksaunders%2Fpayload-chat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faaronksaunders%2Fpayload-chat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronksaunders%2Fpayload-chat/lists"}