├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app ├── client-side │ └── page.tsx ├── globals.css ├── head.tsx ├── layout.tsx ├── page.module.css ├── page.tsx ├── realtime │ ├── page.tsx │ └── realtime-posts.tsx ├── server-rendered │ ├── [id] │ │ └── page.tsx │ └── page.tsx ├── static-with-revalidation │ ├── [id] │ │ └── page.tsx │ └── page.tsx └── static │ ├── [id] │ └── page.tsx │ └── page.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages └── api │ └── hello.ts ├── public ├── favicon.ico └── vercel.svg ├── tsconfig.json └── utils └── supabase.ts /.env.local.example: -------------------------------------------------------------------------------- 1 | # these values can be found in your project's API settings 2 | # https://app.supabase.com/project/_/settings/api 3 | 4 | NEXT_PUBLIC_SUPABASE_URL=your-supabase-url 5 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is an example repo to show how to fetch and cache Supabase data with Next.js Server Components. 2 | 3 | Check out [the article](https://supabase.com/blog/fetching-and-caching-supabase-data-in-next-js-server-components) to learn more. 4 | -------------------------------------------------------------------------------- /app/client-side/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import supabase from "../../utils/supabase"; 5 | 6 | export default function ClientPosts() { 7 | const [isLoading, setIsLoading] = useState(true); 8 | const [posts, setPosts] = useState([]); 9 | 10 | useEffect(() => { 11 | const fetchPosts = async () => { 12 | const { data } = await supabase.from("posts").select(); 13 | setPosts(data); 14 | setIsLoading(false); 15 | }; 16 | 17 | fetchPosts(); 18 | }, []); 19 | 20 | return isLoading ? ( 21 |

Loading

22 | ) : ( 23 |
{JSON.stringify(posts, null, 2)}
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Create Next App 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 10 | {/* 11 | will contain the components returned by the nearest parent 12 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 13 | */} 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title { 32 | margin: 0; 33 | line-height: 1.15; 34 | font-size: 4rem; 35 | font-style: normal; 36 | font-weight: 800; 37 | letter-spacing: -0.025em; 38 | } 39 | 40 | .title a { 41 | text-decoration: none; 42 | color: #0070f3; 43 | } 44 | 45 | .title a:hover, 46 | .title a:focus, 47 | .title a:active { 48 | text-decoration: underline; 49 | } 50 | 51 | .title, 52 | .description { 53 | text-align: center; 54 | } 55 | 56 | .description { 57 | margin: 4rem 0; 58 | line-height: 1.5; 59 | font-size: 1.5rem; 60 | } 61 | 62 | .code { 63 | background: #fafafa; 64 | border-radius: 5px; 65 | padding: 0.75rem; 66 | font-size: 1.1rem; 67 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 68 | Bitstream Vera Sans Mono, Courier New, monospace; 69 | } 70 | 71 | .grid { 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | flex-wrap: wrap; 76 | max-width: 1200px; 77 | } 78 | 79 | .card { 80 | margin: 1rem; 81 | padding: 1.5rem; 82 | text-align: left; 83 | color: inherit; 84 | text-decoration: none; 85 | border: 1px solid #eaeaea; 86 | border-radius: 10px; 87 | transition: color 0.15s ease, border-color 0.15s ease; 88 | max-width: 300px; 89 | } 90 | 91 | .card:hover, 92 | .card:focus, 93 | .card:active { 94 | color: #0070f3; 95 | border-color: #0070f3; 96 | } 97 | 98 | .card h2 { 99 | margin: 0 0 1rem 0; 100 | font-size: 1.5rem; 101 | } 102 | 103 | .card p { 104 | margin: 0; 105 | font-size: 1.25rem; 106 | line-height: 1.5; 107 | } 108 | 109 | .logo { 110 | height: 1em; 111 | margin-left: 0.5rem; 112 | } 113 | 114 | @media (max-width: 600px) { 115 | .grid { 116 | width: 100%; 117 | flex-direction: column; 118 | } 119 | } 120 | 121 | @media (prefers-color-scheme: dark) { 122 | .title { 123 | background: linear-gradient(180deg, #ffffff 0%, #aaaaaa 100%); 124 | -webkit-background-clip: text; 125 | -webkit-text-fill-color: transparent; 126 | background-clip: text; 127 | text-fill-color: transparent; 128 | } 129 | .title a { 130 | background: linear-gradient(180deg, #0070f3 0%, #0153af 100%); 131 | -webkit-background-clip: text; 132 | -webkit-text-fill-color: transparent; 133 | background-clip: text; 134 | text-fill-color: transparent; 135 | } 136 | .card, 137 | .footer { 138 | border-color: #222; 139 | } 140 | .code { 141 | background: #111; 142 | } 143 | .logo img { 144 | filter: invert(1); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import styles from './page.module.css' 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

9 | Welcome to Next.js 13! 10 |

11 | 12 |

13 | Get started by editing{' '} 14 | app/page.tsx 15 |

16 | 17 |
18 | 19 |

Documentation →

20 |

Find in-depth information about Next.js 13

21 |
22 | 23 | 27 |

Examples →

28 |

Explore the Next.js 13 playground.

29 |
30 | 31 | 37 |

Deploy →

38 |

Deploy your Next.js site to a public URL with Vercel.

39 |
40 |
41 |
42 | 43 | 55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /app/realtime/page.tsx: -------------------------------------------------------------------------------- 1 | import supabase from "../../utils/supabase"; 2 | import RealtimePosts from "./realtime-posts"; 3 | 4 | // do not cache this page 5 | export const revalidate = 0; 6 | 7 | // this component fetches the current posts server-side 8 | // and subscribes to new posts client-side 9 | export default async function Realtime() { 10 | const { data } = await supabase.from("posts").select("*"); 11 | 12 | // data can be passed from server components to client components 13 | // this allows us to fetch the initial posts before rendering the page 14 | // our component will then subscribe to new posts client-side 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /app/realtime/realtime-posts.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import supabase from "../../utils/supabase"; 5 | 6 | // realtime subscriptions need to be set up client-side 7 | // this component takes initial posts as props and automatically 8 | // updates when new posts are inserted into Supabase's `posts` table 9 | export default function RealtimePosts({ serverPosts }: { serverPosts: any }) { 10 | const [posts, setPosts] = useState(serverPosts); 11 | 12 | useEffect(() => { 13 | // this overwrites `posts` any time the `serverPosts` prop changes 14 | // this happens when the parent Server Component is re-rendered 15 | setPosts(serverPosts); 16 | }, [serverPosts]); 17 | 18 | useEffect(() => { 19 | // ensure you have enabled replication on the `posts` table 20 | // https://app.supabase.com/project/_/database/replication 21 | const channel = supabase 22 | .channel("*") 23 | .on( 24 | "postgres_changes", 25 | { event: "INSERT", schema: "public", table: "posts" }, 26 | (payload) => setPosts((posts: any) => [...posts, payload.new]) 27 | ) 28 | .subscribe(); 29 | 30 | return () => { 31 | supabase.removeChannel(channel); 32 | }; 33 | }, [serverPosts]); 34 | 35 | return
{JSON.stringify(posts, null, 2)}
; 36 | } 37 | -------------------------------------------------------------------------------- /app/server-rendered/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import supabase from "../../../utils/supabase"; 2 | import { notFound } from "next/navigation"; 3 | 4 | // do not cache this page 5 | export const revalidate = 0; 6 | 7 | export async function generateStaticParams() { 8 | const { data: posts } = await supabase.from("posts").select("id"); 9 | 10 | return posts?.map(({ id }) => ({ 11 | id, 12 | })); 13 | } 14 | 15 | export default async function Post({ 16 | params: { id }, 17 | }: { 18 | params: { id: string }; 19 | }) { 20 | const { data: post } = await supabase 21 | .from("posts") 22 | .select() 23 | .match({ id }) 24 | .single(); 25 | 26 | if (!post) { 27 | notFound(); 28 | } 29 | 30 | return
{JSON.stringify(post, null, 2)}
; 31 | } 32 | -------------------------------------------------------------------------------- /app/server-rendered/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import supabase from "../../utils/supabase"; 3 | 4 | // do not cache this page 5 | export const revalidate = 0; 6 | 7 | export default async function Posts() { 8 | const { data: posts } = await supabase.from("posts").select("id, title"); 9 | 10 | if (!posts) { 11 | return

No posts found.

; 12 | } 13 | 14 | return posts.map((post) => ( 15 |

16 | {post.title} 17 |

18 | )); 19 | } 20 | -------------------------------------------------------------------------------- /app/static-with-revalidation/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import supabase from "../../../utils/supabase"; 2 | import { notFound } from "next/navigation"; 3 | 4 | // cache this page for 1 minute 5 | export const revalidate = 60; 6 | 7 | export async function generateStaticParams() { 8 | const { data: posts } = await supabase.from("posts").select("id"); 9 | 10 | return posts?.map(({ id }) => ({ 11 | id, 12 | })); 13 | } 14 | 15 | export default async function Post({ 16 | params: { id }, 17 | }: { 18 | params: { id: string }; 19 | }) { 20 | const { data: post } = await supabase 21 | .from("posts") 22 | .select() 23 | .match({ id }) 24 | .single(); 25 | 26 | if (!post) { 27 | notFound(); 28 | } 29 | 30 | return
{JSON.stringify(post, null, 2)}
; 31 | } 32 | -------------------------------------------------------------------------------- /app/static-with-revalidation/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import supabase from "../../utils/supabase"; 3 | 4 | // cache this page for 1 minute 5 | export const revalidate = 60; 6 | 7 | export default async function Posts() { 8 | const { data: posts } = await supabase.from("posts").select("id, title"); 9 | 10 | if (!posts) { 11 | return

No posts found.

; 12 | } 13 | 14 | return posts.map((post) => ( 15 |

16 | {post.title} 17 |

18 | )); 19 | } 20 | -------------------------------------------------------------------------------- /app/static/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import supabase from "../../../utils/supabase"; 2 | import { notFound } from "next/navigation"; 3 | 4 | export async function generateStaticParams() { 5 | const { data: posts } = await supabase.from("posts").select("id"); 6 | 7 | return posts?.map(({ id }) => ({ 8 | id, 9 | })); 10 | } 11 | 12 | export default async function Post({ 13 | params: { id }, 14 | }: { 15 | params: { id: string }; 16 | }) { 17 | const { data: post } = await supabase 18 | .from("posts") 19 | .select() 20 | .match({ id }) 21 | .single(); 22 | 23 | if (!post) { 24 | notFound(); 25 | } 26 | 27 | return
{JSON.stringify(post, null, 2)}
; 28 | } 29 | -------------------------------------------------------------------------------- /app/static/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import supabase from "../../utils/supabase"; 3 | 4 | export default async function Posts() { 5 | const { data: posts } = await supabase.from("posts").select("id, title"); 6 | 7 | if (!posts) { 8 | return

No posts found.

; 9 | } 10 | 11 | return posts.map((post) => ( 12 |

13 | {post.title} 14 |

15 | )); 16 | } 17 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next13", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@supabase/supabase-js": "^2.1.0", 13 | "@types/node": "18.11.9", 14 | "@types/react": "18.0.25", 15 | "@types/react-dom": "18.0.9", 16 | "eslint": "8.27.0", 17 | "eslint-config-next": "13.0.3", 18 | "next": "13.0.3", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "typescript": "4.8.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dijonmusters/fetching-and-caching-supabase-data-in-next-js-13-server-components/9b7d19ab165dda7943af5fad7aaf2f1bb423eade/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /utils/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | export default createClient( 4 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 5 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 6 | ); 7 | --------------------------------------------------------------------------------