{"id":14968846,"url":"https://github.com/lawrencecchen/threaded-comments","last_synced_at":"2026-03-01T13:03:33.218Z","repository":{"id":43799354,"uuid":"353559597","full_name":"lawrencecchen/threaded-comments","owner":"lawrencecchen","description":"Reddit styled threaded comments using Supabase and Next.js","archived":false,"fork":false,"pushed_at":"2022-01-14T07:10:29.000Z","size":304,"stargazers_count":201,"open_issues_count":2,"forks_count":18,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-26T05:38:37.765Z","etag":null,"topics":["comments","nextjs","supabase","threaded-comments"],"latest_commit_sha":null,"homepage":"https://debussy.vercel.app","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/lawrencecchen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-04-01T03:19:07.000Z","updated_at":"2025-09-13T04:25:49.000Z","dependencies_parsed_at":"2022-08-31T03:51:28.343Z","dependency_job_id":null,"html_url":"https://github.com/lawrencecchen/threaded-comments","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lawrencecchen/threaded-comments","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lawrencecchen%2Fthreaded-comments","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lawrencecchen%2Fthreaded-comments/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lawrencecchen%2Fthreaded-comments/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lawrencecchen%2Fthreaded-comments/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lawrencecchen","download_url":"https://codeload.github.com/lawrencecchen/threaded-comments/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lawrencecchen%2Fthreaded-comments/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29969700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T12:56:10.327Z","status":"ssl_error","status_checked_at":"2026-03-01T12:55:24.744Z","response_time":124,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["comments","nextjs","supabase","threaded-comments"],"created_at":"2024-09-24T13:40:41.615Z","updated_at":"2026-03-01T13:03:33.201Z","avatar_url":"https://github.com/lawrencecchen.png","language":"TypeScript","readme":"# Reddit styled threaded comments\n\n## Demo\n\nhttps://debussy.vercel.app\n\n\n\nhttps://user-images.githubusercontent.com/54008264/116842442-7109b000-ab91-11eb-93d5-570f20f139bd.mov\n\n\n\n\n## Features\n\n- 🧵 Threaded comments (nesting!!)\n- 🗳 Voting\n- 🥇 Sorting\n- 📑 Pagination\n- 🌒 Dark mode\n\n## Instant Deploy\n\nThe Vercel deployment will guide you through creating a Supabase account and project. After you install the Supabase integration, all relevant environment variables will be set up so that the project is immediately live 🚀.\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Flawrencecchen%2Fthreaded-comments\u0026demo-title=Threaded%20Comments%20Demo\u0026demo-description=Threaded%20comments%20built%20with%20Supabase%20and%20Next.js\u0026demo-url=https%3A%2F%2Fdebussy.vercel.app%2F\u0026integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv\u0026external-id=threaded-comments)\n\n## Getting started\n\n### 1. Populate `.env`\n```\nNEXT_PUBLIC_SUPABASE_URL=https://[YOUR_PROJECT_ID].supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=\n```\n\n### 2. Run [setup.sql](sql/setup.sql)\n\n\u003cdetails\u003e\u003csummary\u003eExpand for setup.sql\u003c/summary\u003e\n\n```sql\n-- Create a table for Public Profiles\ncreate table profiles (\n  id uuid references auth.users not null,\n  updated_at timestamp with time zone,\n  username text unique,\n  full_name text,\n  avatar_url text,\n  website text,\n\n  primary key (id),\n  unique (username),\n  constraint username_length check (char_length(username) \u003e= 3)\n);\n\nalter table profiles enable row level security;\n\ncreate policy \"Public profiles are viewable by everyone.\"\n  on profiles for select\n  using ( true );\n\ncreate policy \"Users can insert their own profile.\"\n  on profiles for insert\n  with check ( auth.uid() = id );\n\ncreate policy \"Users can update own profile.\"\n  on profiles for update\n  using ( auth.uid() = id );\n\n-- Create a trigger to sync profiles and auth.users\ncreate function public.handle_new_user()\nreturns trigger as $$\nbegin\n  insert into public.profiles (id, full_name, avatar_url)\n  values (new.id, new.raw_user_meta_data-\u003e\u003e'full_name', new.raw_user_meta_data-\u003e\u003e'avatar_url');\n  return new;\nend;\n$$ language plpgsql security definer;\n\ncreate trigger on_auth_user_created\n  after insert on auth.users\n  for each row execute procedure public.handle_new_user();\n\n\n-- Create a table for posts\ncreate table posts (\n    id bigserial not null,\n    slug text not null unique,\n    \"createdAt\" timestamp with time zone default now() not null,\n    \"updatedAt\" timestamp with time zone default now() null,\n    title text null,\n    content text null,\n    \"isPublished\" boolean default false not null,\n    \"authorId\" uuid not null references profiles (id),\n    \"parentId\" bigint null references posts (id),\n    live boolean default true null,\n    \"siteId\" bigint default '1' ::bigint not null,\n    \"isPinned\" boolean default false not null,\n    \"isDeleted\" boolean default false not null,\n    \"isApproved\" boolean default false not null,\n\n    primary key (id),\n    unique (slug)\n);\n\nalter table posts enable row level security;\n\n-- Create root user and initial post. Triggers update in profiles.\ninsert into auth.users (id) values ('00000000-0000-0000-0000-000000000000'::uuid);\n\nupdate profiles\nset\n    full_name = 'Admin',\n    avatar_url = 'https://assets3.thrillist.com/v1/image/1875552/414x310/crop;jpeg_quality=65.jpg'\nwhere\n    id = '00000000-0000-0000-0000-000000000000'::uuid;\n\ninsert into posts (slug, title, content, \"authorId\") values ('root', 'Root post', 'root post', '00000000-0000-0000-0000-000000000000'::uuid);\ninsert into posts (slug, title, content, \"authorId\", \"parentId\") values ('threaded-comments-123', 'threaded-comments', 'Threaded comments, built on top of Supabase and Next.js. Visit GitHub to deploy your own: https://github.com/lawrencecchen/threaded-comments', '00000000-0000-0000-0000-000000000000'::uuid, 1);\n\ncreate policy \"Posts are viewable by everyone.\"\n    on posts for select\n    using ( true );\n\ncreate policy \"Users can post as themselves.\"\n    on posts for insert\n    with check ( auth.uid() = \"authorId\" );\n\n-- Create a table for sites\ncreate table sites (\n    id bigserial not null,\n    \"siteDomain\" text not null,\n    \"ownerId\" uuid not null,\n    \"name\" text not null,\n\n    primary key (id)\n);\n\nalter table sites enable row level security;\n\ncreate policy \"Sites are viewable by everyone.\"\n    on sites for select\n    using ( true );\n\ncreate policy \"Users can create their own sites.\"\n    on sites for insert\n    with check ( auth.uid() = \"ownerId\" );\n\n-- Create a table for votes\ncreate table votes (\n    \"postId\" bigint not null references posts (id),\n    \"userId\" uuid not null references profiles (id),\n    \"value\" int not null,\n\n    primary key (\"postId\", \"userId\"),\n    constraint vote_quantity check (value \u003c= 1 and value \u003e= -1)\n);\n\nalter table votes enable row level security;\n\ncreate policy \"Votes are viewable by everyone\"\n    on votes for select\n    using ( true );\n\ncreate policy \"Users can vote as themselves\"\n    on votes for insert\n    with check (auth.uid() = \"userId\");\n\ncreate policy \"Users can update their own votes\"\n    on votes for update\n    using ( auth.uid() = \"userId\" );\n\n\n-- Set up Realtime!\nbegin;\n  drop publication if exists supabase_realtime;\n  create publication supabase_realtime;\ncommit;\nalter publication supabase_realtime add table posts, sites, votes, profiles;\n\n-- Set up Storage!\ninsert into storage.buckets (id, name)\nvalues ('avatars', 'avatars');\n\ncreate policy \"Avatar images are publicly accessible.\"\n  on storage.objects for select\n  using ( bucket_id = 'avatars' );\n\ncreate policy \"Anyone can upload an avatar.\"\n  on storage.objects for insert\n  with check ( bucket_id = 'avatars' );\n\ndrop view if exists comments_thread_with_user_vote;\ndrop view if exists comments_thread;\ndrop view if exists comments_with_author_votes;\ndrop view if exists comments_linear_view;\ndrop view if exists comment_with_author;\n\ncreate view comment_with_author as\n    select\n        p.id,\n        p.slug,\n        p.\"createdAt\",\n        p.\"updatedAt\",\n        p.title,\n        p.content,\n        p.\"isPublished\",\n        p.\"authorId\",\n        p.\"parentId\",\n        p.live,\n        p.\"siteId\",\n        p.\"isPinned\",\n        p.\"isDeleted\",\n        p.\"isApproved\",\n        to_jsonb(u) as author\n    from\n        posts p\n        inner join profiles u on p.\"authorId\" = u.id;\n\ncreate view comments_linear_view as\n    select\n        root_c.*,\n        to_jsonb(parent_c) as parent,\n        coalesce(json_agg(children_c) filter (where children_c.id is not null), '[]') as responses\n    from\n        comment_with_author root_c\n        inner join comment_with_author parent_c on root_c.\"parentId\" = parent_c.id\n        inner join sites s1 on s1.id = root_c.\"siteId\"\n        left join comment_with_author children_c on children_c.\"parentId\" = root_c.id\n    group by\n        root_c.id,\n        root_c.slug,\n        root_c.\"createdAt\",\n        root_c.\"updatedAt\",\n        root_c.title,\n        root_c.content,\n        root_c.\"isPublished\",\n        root_c.\"authorId\",\n        root_c.\"parentId\",\n        root_c.live,\n        root_c.\"siteId\",\n        root_c.\"isPinned\",\n        root_c.\"isDeleted\",\n        root_c.\"isApproved\",\n        root_c.author,\n        parent_c.*;\n\ncreate or replace view comments_with_author_votes as\n    select\n        p.id,\n        p.slug,\n        p.\"createdAt\",\n        p.\"updatedAt\",\n        p.title,\n        p.content,\n        p.\"isPublished\",\n        p.\"authorId\",\n        p.\"parentId\",\n        p.live,\n        p.\"siteId\",\n        p.\"isPinned\",\n        p.\"isDeleted\",\n        p.\"isApproved\",\n        p.\"author\",\n        coalesce (\n            sum (v.value) over w,\n            0\n        ) as \"votes\",\n        sum (case when v.value \u003e 0 then 1 else 0 end) over w as \"upvotes\",\n        sum (case when v.value \u003c 0 then 1 else 0 end) over w as \"downvotes\"\n        -- (select case when auth.uid() = v.\"userId\" then v.value else 0 end) as \"userVoteValue\"\n    from\n        comment_with_author p\n        left join votes v on p.id = v.\"postId\"\n    window w as (\n        partition by v.\"postId\"\n    );\n\ncreate recursive view comments_thread (\n    id,\n    slug,\n    \"createdAt\",\n    \"updatedAt\",\n    title,\n    content,\n    \"isPublished\",\n    \"authorId\",\n    \"parentId\",\n    live,\n    \"siteId\",\n    \"isPinned\",\n    \"isDeleted\",\n    \"isApproved\",\n    \"author\",\n    \"votes\",\n    \"upvotes\",\n    \"downvotes\",\n    \"depth\",\n    \"path\",\n    \"pathVotesRecent\",\n    \"pathLeastRecent\",\n    \"pathMostRecent\"\n) as\n    select\n        id,\n        slug,\n        \"createdAt\",\n        \"updatedAt\",\n        title,\n        content,\n        \"isPublished\",\n        \"authorId\",\n        \"parentId\",\n        live,\n        \"siteId\",\n        \"isPinned\",\n        \"isDeleted\",\n        \"isApproved\",\n        \"author\",\n        \"votes\",\n        \"upvotes\",\n        \"downvotes\",\n        0 as depth,\n        array[id] as \"path\",\n        array[id] as \"pathVotesRecent\",\n        array[id] as \"pathLeastRecent\",\n        array[id] as \"pathMostRecent\"\n    from\n        comments_with_author_votes\n    where\n        \"parentId\" is null\n    union\n    select\n        p1.id,\n        p1.slug,\n        p1.\"createdAt\",\n        p1.\"updatedAt\",\n        p1.title,\n        p1.content,\n        p1.\"isPublished\",\n        p1.\"authorId\",\n        p1.\"parentId\",\n        p1.live,\n        p1.\"siteId\",\n        p1.\"isPinned\",\n        p1.\"isDeleted\",\n        p1.\"isApproved\",\n        p1.\"author\",\n        p1.\"votes\",\n        p1.\"upvotes\",\n        p1.\"downvotes\",\n        p2.depth + 1 as depth,\n        p2.\"path\" || p1.id::bigint as \"path\",\n        p2.\"pathVotesRecent\" || -p1.\"votes\"::bigint || -extract(epoch from p1.\"createdAt\")::bigint || p1.id as \"pathVotesRecent\",\n        p2.\"pathLeastRecent\" || extract(epoch from p1.\"createdAt\")::bigint || p1.id as \"pathLeastRecent\",\n        p2.\"pathMostRecent\" || -extract(epoch from p1.\"createdAt\")::bigint || p1.id as \"pathMostRecent\"\n    from\n        comments_with_author_votes p1\n        join comments_thread p2 on p1.\"parentId\" = p2.id;\n\ncreate or replace view comments_thread_with_user_vote as\n    select distinct on (id)\n        id,\n        slug,\n        \"createdAt\",\n        \"updatedAt\",\n        title,\n        content,\n        \"isPublished\",\n        \"authorId\",\n        \"parentId\",\n        live,\n        \"siteId\",\n        \"isPinned\",\n        \"isDeleted\",\n        \"isApproved\",\n        \"author\",\n        \"votes\",\n        \"upvotes\",\n        \"downvotes\",\n        \"depth\",\n        \"path\",\n        \"pathVotesRecent\",\n        \"pathLeastRecent\",\n        \"pathMostRecent\",\n        coalesce(\n            (\n                select\n                    v.\"value\"\n                from\n                    votes v\n                where\n                    auth.uid() = v.\"userId\" and v.\"postId\" = id\n            ),\n            0\n        ) as \"userVoteValue\"\n    from comments_thread\n\n```\n\u003c/details\u003e\n\n### 3. Run the application\n\n`npm run dev` or `pnpm dev` or `yarn dev`\n\n## License\n[MIT](LICENSE)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flawrencecchen%2Fthreaded-comments","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flawrencecchen%2Fthreaded-comments","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flawrencecchen%2Fthreaded-comments/lists"}