├── .env.sample ├── .eslintrc.json ├── .gitignore ├── README.md ├── components └── clerk │ ├── GithubLink.module.css │ └── GithubLink.tsx ├── docs └── cover.svg ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js └── index.js ├── public ├── arrow-right.svg ├── clerk.svg ├── clerkandsupa.svg ├── favicon.ico └── vercel.svg └── styles ├── Home.module.css └── globals.css /.env.sample: -------------------------------------------------------------------------------- 1 | # Clerk env vars 2 | NEXT_PUBLIC_CLERK_FRONTEND_API= 3 | CLERK_API_KEY= 4 | 5 | # Supabase env vars 6 | NEXT_PUBLIC_SUPABASE_URL= 7 | NEXT_PUBLIC_SUPABASE_KEY= 8 | SUPABASE_JWT_SECRET= -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Supabase + Clerk Todo App 2 | 3 | [![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/clerkinc/clerk-supabase) [![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://docs.clerk.dev?utm_source=github&utm_medium=tutorial_repos&utm_campaign=supabase) [![@ClerkDev on Twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) 4 | 5 | This demo repo represents the final state of the Next.js todo app built with a Supabase database and Clerk for multifactor authentication. You can follow along with [this guide](https://clerk.dev/blog/nextjs-supabase-todos-with-multifactor-authentication?utm_source=github&utm_medium=tutorial_repos&utm_campaign=supabase) to build it yourself. 6 | 7 | ![Build a todo app with Next.js, Supabase and Multifactor auth](./docs/cover.svg) 8 | 9 | ## Contact 10 | 11 | If you need support or have anything you would like to ask, please reach out on our [Discord channel](https://discord.com/invite/b5rXHjAg7A). We'd love to chat! 12 | 13 | -------------------------------------------------------------------------------- /components/clerk/GithubLink.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: "Inter", sans-serif; 3 | font-size: 0.75rem; 4 | font-weight: 400; 5 | line-height: 1rem; 6 | letter-spacing: 0em; 7 | text-align: left; 8 | 9 | display: flex; 10 | 11 | box-shadow: 0px 24px 48px 0px #00000029; 12 | border-radius: 6px; 13 | 14 | height: 40px; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 0 1.5rem; 18 | background-color: white; 19 | } 20 | 21 | .logo { 22 | position: relative; 23 | top: 2px; 24 | min-width: 56px; 25 | } 26 | 27 | .label { 28 | font-family: "Inter", sans-serif; 29 | font-size: 0.75rem; 30 | font-weight: 400; 31 | line-height: 1rem; 32 | letter-spacing: 0em; 33 | text-align: left; 34 | margin: 0 1.5rem; 35 | } 36 | 37 | .rightLink { 38 | color: #335bf1; 39 | min-width: 100px; 40 | } 41 | 42 | .rightLink a:link { 43 | text-decoration: none; 44 | color: #335bf1; 45 | } 46 | 47 | .rightLink a:visited { 48 | text-decoration: none; 49 | color: #335bf1; 50 | } 51 | 52 | .rightLink a:hover { 53 | text-decoration: underline; 54 | color: #335bf1; 55 | } 56 | 57 | .rightLink a:active { 58 | text-decoration: underline; 59 | color: #335bf1; 60 | } 61 | 62 | .rightArrow { 63 | position: relative; 64 | top: 2px; 65 | left: 3px; 66 | } 67 | -------------------------------------------------------------------------------- /components/clerk/GithubLink.tsx: -------------------------------------------------------------------------------- 1 | import styles from './GithubLink.module.css' 2 | import Image from 'next/image' 3 | 4 | type GithubLinkProps = { 5 | label: string 6 | repoLink: string 7 | } 8 | const GithubLink = (props: GithubLinkProps) => ( 9 | <> 10 |
11 | 16 | clerk 17 | 18 |
{props.label}
19 |
20 | 21 | View on Github 22 | 23 | -> 24 | 25 | 26 |
27 |
28 | 29 | ) 30 | 31 | export default GithubLink 32 | -------------------------------------------------------------------------------- /docs/cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supabase-clerk", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@clerk/nextjs": "^3.0.0-alpha.3", 12 | "@supabase/supabase-js": "^2.0.0", 13 | "jsonwebtoken": "^8.5.1", 14 | "next": "^12.0.9", 15 | "react": "17.0.2", 16 | "react-dom": "17.0.2" 17 | }, 18 | "devDependencies": { 19 | "eslint": "8.4.1", 20 | "eslint-config-next": "12.0.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import GithubLink from '../components/clerk/GithubLink.tsx' 3 | import { ClerkProvider } from '@clerk/nextjs' 4 | 5 | function ClerkSupabaseApp({ Component, pageProps }) { 6 | return ( 7 | 8 | 9 | 15 | 16 | ) 17 | } 18 | 19 | export default ClerkSupabaseApp 20 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import styles from '../styles/Home.module.css' 2 | import { useState, useEffect } from 'react' 3 | import { 4 | useSession, 5 | useUser, 6 | UserButton, 7 | SignInButton, 8 | SignUpButton 9 | } from '@clerk/nextjs' 10 | import { createClient } from '@supabase/supabase-js' 11 | 12 | const supabaseClient = async supabaseAccessToken => { 13 | const supabase = createClient( 14 | process.env.NEXT_PUBLIC_SUPABASE_URL, 15 | process.env.NEXT_PUBLIC_SUPABASE_KEY, 16 | { 17 | global: { headers: { Authorization: `Bearer ${supabaseAccessToken}` } } 18 | } 19 | ) 20 | // set Supabase JWT on the client object, 21 | // so it is sent up with all Supabase requests 22 | return supabase 23 | } 24 | 25 | export default function Home() { 26 | const { isSignedIn, isLoading, user } = useUser() 27 | const [todos, setTodos] = useState(null) 28 | return ( 29 | <> 30 |
31 | {isLoading ? ( 32 | <> 33 | ) : ( 34 |
35 |
36 | {isSignedIn ? ( 37 | <> 38 |
Welcome {user.firstName}!
39 | 40 | 41 | 42 | ) : ( 43 |
44 | Sign in to create your todo list! 45 |
46 | )} 47 |
48 |
49 | )} 50 | 51 | ) 52 | } 53 | 54 | const Header = () => { 55 | const { isSignedIn } = useUser() 56 | 57 | return ( 58 |
59 |
My Todo App
60 | {isSignedIn ? ( 61 | 62 | ) : ( 63 |
64 | 65 |   66 | 67 |
68 | )} 69 |
70 | ) 71 | } 72 | 73 | const TodoList = ({ todos, setTodos }) => { 74 | const { session } = useSession() 75 | const [loadingTodos, setLoadingTodos] = useState(true) 76 | 77 | // on first load, fetch and set todos 78 | useEffect(() => { 79 | const loadTodos = async () => { 80 | try { 81 | setLoadingTodos(true) 82 | const supabaseAccessToken = await session.getToken({ 83 | template: 'Supabase' 84 | }) 85 | const supabase = await supabaseClient(supabaseAccessToken) 86 | const { data: todos } = await supabase.from('todos').select('*') 87 | setTodos(todos) 88 | } catch (e) { 89 | alert(e) 90 | } finally { 91 | setLoadingTodos(false) 92 | } 93 | } 94 | loadTodos() 95 | }, []) 96 | 97 | // if loading, just show basic message 98 | if (loadingTodos) { 99 | return
Loading...
100 | } 101 | 102 | // display all the todos 103 | return ( 104 | <> 105 | {todos?.length > 0 ? ( 106 |
107 |
    108 | {todos.map(todo => ( 109 |
  1. {todo.title}
  2. 110 | ))} 111 |
112 |
113 | ) : ( 114 |
You don't have any todos!
115 | )} 116 | 117 | ) 118 | } 119 | 120 | function AddTodoForm({ todos, setTodos }) { 121 | const { session } = useSession() 122 | const [newTodo, setNewTodo] = useState('') 123 | const handleSubmit = async e => { 124 | e.preventDefault() 125 | if (newTodo === '') { 126 | return 127 | } 128 | 129 | const supabaseAccessToken = await session.getToken({ 130 | template: 'Supabase' 131 | }) 132 | const supabase = await supabaseClient(supabaseAccessToken) 133 | const { data } = await supabase 134 | .from('todos') 135 | .insert({ title: newTodo, user_id: session.user.id }) 136 | .select() 137 | 138 | setTodos([...todos, data[0]]) 139 | setNewTodo('') 140 | } 141 | 142 | return ( 143 |
144 | setNewTodo(e.target.value)} value={newTodo} /> 145 |   146 |
147 | ) 148 | } 149 | -------------------------------------------------------------------------------- /public/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/clerk.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/clerkandsupa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-supabase/a6bf62c35aa996ed7b6308ea1788a503f0783d24/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 1rem 2rem; 3 | height: 4rem; 4 | background-color: lightgray; 5 | width: 100%; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .main { 12 | margin: auto; 13 | max-width: 400px; 14 | } 15 | 16 | .label { 17 | padding: 1rem 2rem; 18 | } 19 | 20 | .container { 21 | padding: 1rem 2rem; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | } 26 | 27 | .todoList { 28 | padding-left: 1rem; 29 | align-self: baseline; 30 | } 31 | -------------------------------------------------------------------------------- /styles/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 | footer { 19 | position: fixed; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | margin: 20px; 27 | } 28 | --------------------------------------------------------------------------------