{"id":15857323,"url":"https://github.com/timmikeladze/next-realtime","last_synced_at":"2025-07-23T14:34:37.180Z","repository":{"id":214839815,"uuid":"737264243","full_name":"TimMikeladze/next-realtime","owner":"TimMikeladze","description":null,"archived":false,"fork":false,"pushed_at":"2023-12-31T09:16:58.000Z","size":3964,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-07-22T00:47:46.972Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TimMikeladze.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-12-30T11:55:24.000Z","updated_at":"2024-04-17T11:26:55.000Z","dependencies_parsed_at":"2023-12-31T09:33:50.470Z","dependency_job_id":null,"html_url":"https://github.com/TimMikeladze/next-realtime","commit_stats":null,"previous_names":["timmikeladze/next-realtime"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TimMikeladze/next-realtime","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TimMikeladze%2Fnext-realtime","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TimMikeladze%2Fnext-realtime/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TimMikeladze%2Fnext-realtime/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TimMikeladze%2Fnext-realtime/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TimMikeladze","download_url":"https://codeload.github.com/TimMikeladze/next-realtime/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TimMikeladze%2Fnext-realtime/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266483338,"owners_count":23936307,"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","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2024-10-05T20:22:16.403Z","updated_at":"2025-07-23T14:34:37.160Z","avatar_url":"https://github.com/TimMikeladze.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⌚️ next-realtime\n\nDrop-in realtime data for the unstable Next.js cache.\n\nExamples include:\n\n- A comment section with live updates.\n- A collaborative todo list.\n- A multiplayer card game.\n- Real-time data updates where the data is not updated on a regular interval and time-based invalidation isn't an preferable solution.\n\n\u003e 🔬 This is a highly experimental project.\n\nThis project is a proof-of-concept of drop-in solution for adding realtime data to a Next.js app. The goal is to provide a low overhead solution for adding realtime data to a Next.js app by leveraging the unstable Next.js cache, HTTP Streaming and a message broker like Redis.\n\nInstead of streaming realtime updates to the client directly, the client is subscribed to a stream of cache tags that have been invalidated. The client then uses the Next.js API to revalidate the cache for each tag. React Server Components which rely on data from the `unstable_cache` are then re-rendered with the latest data.\n\nCheck out this [example](https://github.com/TimMikeladze/next-realtime/tree/master/examples/next-realtime-example) of a Next.js codebase showcasing an implementation of a simple Todo-list app using `next-realtime`.\n\n\u003e 🚧 Under active development. Expect breaking changes until v1.0.0.\n\n## 📡 Install\n\n```console\nnpm install next-realtime\n\nyarn add next-realtime\n\npnpm add next-realtime\n```\n\n\u003e 👋 Hello there! Follow me [@linesofcode](https://twitter.com/linesofcode) or visit [linesofcode.dev](https://linesofcode.dev) for more cool projects like this one.\n\n## 🚀 Getting Started\n\nFirst, configure `next-realtime` by providing some required values like specifying a message broker.\n\n**app/realtime/config.ts**\n\n```ts\n// Supports Redis or Postgres as a message broker.\nimport Redis from 'ioredis';\nimport postgres from 'postgres';\nimport {\n  configNextRealtimeRedis,\n  configNextRealtimePostgres,\n} from 'next-realtime/server';\n\nexport const redis = new Redis(process.env.REDIS_CONNECTION_STRING as string);\n// or\nexport const client = postgres(\n  `${process.env.PG_CONNECTION_STRING}/${process.env.PG_DB}`\n);\n\nexport const { NextRealtimeStreamHandler, revalidateRealtimeTag } =\n  configNextRealtimeRedis(redis);\n```\n\nNow create a route that will stream data to the client.\n\n**app/realtime/route.ts**\n\n```ts\nimport { NextRequest } from 'next/server';\nimport { NextRealtimeStreamHandler } from './config';\n\nexport const dynamic = 'force-dynamic';\n\nexport const GET = (request: NextRequest) =\u003e NextRealtimeStreamHandler(request);\n```\n\nSomewhere near the root of your app, inside of a React Server Component, render a `NextRealtimeStreamProvider`. This component will handle subscribing to the stream of tags and revalidating the cache.\n\n**app/realtime/layout.ts**\n\n```tsx\nimport { NextRealtimeStreamProvider } from 'next-realtime/react';\nimport { createRealtimeSessionId } from 'next-realtime/server';\n\nimport { revalidateTag } from 'next/cache';\n\nimport { ReactNode } from 'react';\n\nimport type { Metadata } from 'next';\nimport { Inter } from 'next/font/google';\nimport './globals.css';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport const metadata: Metadata = {\n  title: 'Create Next App',\n  description: 'Generated by create next app',\n};\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n  return (\n    \u003cNextRealtimeStreamProvider\n      revalidateTag={async (tag: string) =\u003e {\n        'use server';\n\n        revalidateTag(tag);\n      }}\n      sessionId={async () =\u003e {\n        'use server';\n\n        return createRealtimeSessionId();\n      }}\n    \u003e\n      \u003chtml lang=\"en\"\u003e\n        \u003cbody className={inter.className}\u003e{children}\u003c/body\u003e\n      \u003c/html\u003e\n    \u003c/NextRealtimeStreamProvider\u003e\n  );\n}\n```\n\n## 🛰️ Usage\n\n1. Choose a cache tag whose data you want to make realtime. In this example, we'll use the tag `todos`.\n2. Create an action that will fetch data and pass it through the `unstable_cache` function with the chosen cache tag.\n3. Create a React Server Component that will fetch async data from the action and render it.\n4. Create an action that will add new data to the database and invalidate the chosen cache tag.\n\n**src/actions/getTodos.tsx**\n\n```ts\n'use server';\n\nimport { unstable_cache } from 'next/cache';\nimport { desc } from 'drizzle-orm';\nimport { getDb } from '../drizzle/getDb';\nimport { todoTable } from '../drizzle/schema';\n\nexport const getTodos = unstable_cache(\n  async () =\u003e {\n    const db = await getDb();\n\n    return db.select().from(todoTable).orderBy(desc(todoTable.createdAt));\n  },\n  [],\n  {\n    tags: ['todos'], // 👈 define a tag to make realtime\n  }\n);\n```\n\n**src/components/TodoList.tsx**\n\n```tsx\nimport { getTodos } from '../actions/getTodos'; // 👈\nimport TodoCard from './TodoCard';\nimport { AddTodoButton } from './AddTodoButton';\n\nconst TodoList = async () =\u003e {\n  // 👈 async react component\n  const todos = await getTodos();\n\n  return (\n    \u003cdiv className=\"container p-4\"\u003e\n      \u003cAddTodoButton /\u003e\n      \u003cdiv className=\"space-y-4\"\u003e\n        {todos.map((todo) =\u003e (\n          \u003cTodoCard key={todo.id} todo={todo} /\u003e\n        ))}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n};\n\nexport default TodoList;\n```\n\n**src/actions/addTodo.ts**\n\n```ts\n'use server';\n\nimport { nanoid } from 'nanoid';\nimport { getRealtimeSessionId } from 'next-realtime/server';\nimport { revalidateRealtimeTag } from '../app/realtime/config'; // 👈\nimport { getDb } from '../drizzle/getDb';\nimport { todoTable } from '../drizzle/schema';\n\nexport const addTodo = async ({ text }: { text: string }) =\u003e {\n  if (!text || !text.trim().length) {\n    return;\n  }\n\n  const db = await getDb();\n\n  await db.insert(todoTable).values({\n    id: nanoid(),\n    text: text.trim(),\n    realtimeSessionId: getRealtimeSessionId(),\n  });\n\n  await revalidateRealtimeTag('todos'); // 👈\n};\n```\n\n**src/components/AddTodoButton.tsx**\n\n```tsx\n'use client';\n\nimport React, { useTransition, useState } from 'react';\nimport { nanoid } from 'nanoid';\nimport { addTodo } from '../actions/addTodo';\n\nexport const AddTodoButton = () =\u003e {\n  const [, startTransition] = useTransition();\n  const [todoText, setTodoText] = useState('');\n\n  const handleAddTodo = () =\u003e {\n    startTransition(() =\u003e\n      addTodo({\n        text: todoText || `Random todo ${nanoid(4)}`,\n      })\n    );\n    setTodoText(''); // Clear the input field after adding todo\n  };\n\n  return (\n    \u003cdiv className=\"flex items-center\"\u003e\n      \u003cinput\n        type=\"text\"\n        value={todoText}\n        onChange={(e) =\u003e setTodoText(e.target.value)}\n        className=\"mr-2 p-2 border border-gray-300 rounded focus:outline-none focus:ring focus:border-blue-500 \"\n        placeholder=\"Enter todo\"\n      /\u003e\n      \u003cbutton\n        className=\"bg-blue-500 text-white font-semibold py-2 px-4 rounded focus:outline-none focus:shadow-outline-blue active:bg-blue-600\"\n        type=\"button\"\n        onClick={handleAddTodo}\n      \u003e\n        Add todo\n      \u003c/button\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n\u003c!-- TSDOC_START --\u003e\n\n\u003c!-- TSDOC_END --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimmikeladze%2Fnext-realtime","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimmikeladze%2Fnext-realtime","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimmikeladze%2Fnext-realtime/lists"}