├── .nvmrc
├── .eslintrc.json
├── .husky
└── pre-commit
├── src
├── app
│ ├── favicon.ico
│ ├── twitter-image.png
│ ├── opengraph-image.png
│ ├── test-examples
│ │ ├── page.tsx
│ │ ├── page.test.tsx
│ │ ├── counter.tsx
│ │ └── counter.test.tsx
│ ├── api
│ │ ├── message
│ │ │ └── route.ts
│ │ └── auth
│ │ │ └── callback
│ │ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── login
│ │ └── page.tsx
├── mocks
│ ├── server.ts
│ ├── browser.ts
│ ├── index.ts
│ └── handlers.ts
├── utils
│ ├── tailwind.ts
│ └── supabase.ts
├── components
│ ├── ReactQueryExample.tsx
│ ├── Step.tsx
│ ├── ReactQueryExample.test.tsx
│ ├── AuthButton.tsx
│ ├── Code.tsx
│ ├── ThemeToggle.tsx
│ ├── Header.tsx
│ ├── ui
│ │ ├── button.tsx
│ │ └── dropdown-menu.tsx
│ ├── ConnectSupabaseSteps.tsx
│ ├── SignUpUserSteps.tsx
│ ├── NextLogo.tsx
│ └── SupabaseLogo.tsx
├── providers
│ ├── ThemeProvider.tsx
│ └── ReactQueryProvider.tsx
├── hooks
│ └── useGetMessage.ts
├── test
│ └── test-utils.tsx
└── middleware.ts
├── postcss.config.js
├── .prettierrc.yaml
├── next-env.d.ts
├── .env.example
├── next.config.js
├── .lintstagedrc.js
├── components.json
├── .prettierignore
├── .gitignore
├── jest.setup.ts
├── jest.config.js
├── tsconfig.json
├── .github
└── workflows
│ └── pull-request.yaml
├── jest.polyfills.js
├── LICENSE
├── tailwind.config.js
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.18.1
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | pnpm lint-staged
5 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeltroya/supa-next-starter/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeltroya/supa-next-starter/HEAD/src/app/twitter-image.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michaeltroya/supa-next-starter/HEAD/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | trailingComma: all
2 | tabWidth: 2
3 | semi: false
4 | singleQuote: true
5 | printWidth: 80
6 | plugins:
7 | - prettier-plugin-tailwindcss
8 |
--------------------------------------------------------------------------------
/src/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node'
2 | import { handlers } from './handlers'
3 |
4 | export const server = setupServer(...handlers)
5 |
--------------------------------------------------------------------------------
/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw/browser'
2 | import { handlers } from './handlers'
3 |
4 | export const worker = setupWorker(...handlers)
5 |
--------------------------------------------------------------------------------
/src/app/test-examples/page.tsx:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | title: 'App Router',
3 | }
4 |
5 | export default function Page() {
6 | return
App Router
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/api/message/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 |
3 | export async function GET() {
4 | return NextResponse.json({ message: 'Hello from the API!' })
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/tailwind.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
5 |
--------------------------------------------------------------------------------
/src/mocks/index.ts:
--------------------------------------------------------------------------------
1 | if (typeof window === 'undefined') {
2 | const { server } = require('./server')
3 | server.listen()
4 | } else {
5 | const { worker } = require('./browser')
6 | worker.start()
7 | }
8 | export {}
9 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Update these with your Supabase details from your project settings > API
2 | # https://app.supabase.com/project/_/settings/api
3 | NEXT_PUBLIC_SUPABASE_URL=your-project-url
4 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
5 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({
2 | enabled: process.env.ANALYZE === 'true',
3 | })
4 |
5 | /** @type {import('next').NextConfig} */
6 | const nextConfig = {}
7 |
8 | module.exports = withBundleAnalyzer(nextConfig)
9 |
--------------------------------------------------------------------------------
/src/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { HttpResponse, http } from 'msw'
2 |
3 | export const handlers = [
4 | // Intercept the "GET /message" request.
5 | http.get('/api/message', () =>
6 | HttpResponse.json({ message: 'Hello from the handler!' }),
7 | ),
8 | ]
9 |
--------------------------------------------------------------------------------
/src/app/test-examples/page.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@/test/test-utils'
2 | import Page from './page'
3 |
4 | it('App Router: Works with Server Components', () => {
5 | render( )
6 | expect(screen.getByRole('heading')).toHaveTextContent('App Router')
7 | })
8 |
--------------------------------------------------------------------------------
/src/components/ReactQueryExample.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import useGetMessage from '@/hooks/useGetMessage'
4 |
5 | const ReactQueryExample = () => {
6 | const { isLoading, data } = useGetMessage()
7 |
8 | if (isLoading) return Loading...
9 |
10 | return {data?.message}
11 | }
12 |
13 | export default ReactQueryExample
14 |
--------------------------------------------------------------------------------
/src/app/test-examples/counter.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 |
5 | export default function Counter() {
6 | const [count, setCount] = useState(0)
7 | return (
8 | <>
9 | {count}
10 | setCount(count + 1)}>
11 | +
12 |
13 | >
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const buildEslintCommand = (filenames) =>
4 | `next lint --fix --file ${filenames
5 | .map((f) => path.relative(process.cwd(), f))
6 | .join(' --file ')}`
7 |
8 | module.exports = {
9 | '*.{js,jsx,ts,tsx}': [
10 | buildEslintCommand,
11 | 'prettier --ignore-path .gitignore --write',
12 | ],
13 | }
14 |
--------------------------------------------------------------------------------
/src/providers/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
4 | import { type ThemeProviderProps } from 'next-themes/dist/types'
5 |
6 | const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => (
7 | {children}
8 | )
9 |
10 | export default ThemeProvider
11 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/utils/tailwind"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/test-examples/counter.test.tsx:
--------------------------------------------------------------------------------
1 | import { userEvent, render, screen } from '@/test/test-utils'
2 | import Counter from './counter'
3 |
4 | it('App Router: Works with Client Components (React State)', async () => {
5 | render( )
6 |
7 | expect(screen.getByText('0')).toBeInTheDocument()
8 |
9 | await userEvent.click(screen.getByRole('button'))
10 |
11 | expect(screen.getByText('1')).toBeInTheDocument()
12 | })
13 |
--------------------------------------------------------------------------------
/src/providers/ReactQueryProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4 | import { useState } from 'react'
5 |
6 | const ReactQueryProvider = ({ children }: { children: React.ReactNode }) => {
7 | const [queryClient] = useState(() => new QueryClient())
8 |
9 | return (
10 | {children}
11 | )
12 | }
13 |
14 | export default ReactQueryProvider
15 |
--------------------------------------------------------------------------------
/src/hooks/useGetMessage.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryResult, useQuery } from '@tanstack/react-query'
2 | import axios from 'axios'
3 |
4 | // Creating a reusable hook to get messages, need to use axios here since Next patches fetch
5 | // and causes issues with msw
6 | const useGetMessage = (): UseQueryResult<{ message: string }, Error> =>
7 | useQuery({
8 | queryKey: ['/api/message'],
9 | queryFn: async () => {
10 | const { data } = await axios.get('/api/message')
11 |
12 | return data
13 | },
14 | })
15 |
16 | export default useGetMessage
17 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
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 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 |
38 | pnpm-lock.yaml
39 |
40 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # IDE
38 | .vscode
39 |
40 | # swc
41 | .swc
--------------------------------------------------------------------------------
/jest.setup.ts:
--------------------------------------------------------------------------------
1 | // Learn more: https://github.com/testing-library/jest-dom
2 | import { server } from '@/mocks/server'
3 | import { QueryCache } from '@tanstack/react-query'
4 | import '@testing-library/jest-dom'
5 |
6 | const queryCache = new QueryCache()
7 | // Establish API mocking before all tests.
8 | beforeAll(() => server.listen())
9 | // Reset any request handlers that we may add during the tests,
10 | // so they don't affect other tests.
11 | afterEach(() => {
12 | server.resetHandlers()
13 | queryCache.clear()
14 | })
15 | // Clean up after the tests are finished.
16 | afterAll(() => server.close())
17 |
--------------------------------------------------------------------------------
/src/components/Step.tsx:
--------------------------------------------------------------------------------
1 | export default function Step({
2 | title,
3 | children,
4 | }: {
5 | title: string
6 | children: React.ReactNode
7 | }) {
8 | return (
9 |
10 |
11 |
15 | {title}
16 |
17 |
18 | {children}
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const nextJest = require('next/jest')
2 |
3 | const createJestConfig = nextJest({
4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
5 | dir: './',
6 | })
7 |
8 | // Add any custom config to be passed to Jest
9 | const customJestConfig = {
10 | setupFilesAfterEnv: ['/jest.setup.ts'],
11 | setupFiles: ['/jest.polyfills.js'],
12 | moduleNameMapper: {
13 | '^@/(.*)$': '/src/$1',
14 | },
15 | testEnvironment: 'jsdom',
16 | transform: {
17 | '^.+\\.(t|j)sx?$': '@swc/jest',
18 | },
19 | testEnvironmentOptions: {
20 | customExportConditions: [''],
21 | },
22 | }
23 |
24 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
25 | module.exports = createJestConfig(customJestConfig)
26 |
--------------------------------------------------------------------------------
/src/test/test-utils.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react'
2 | import { render, RenderOptions } from '@testing-library/react'
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4 |
5 | const queryClient = new QueryClient({
6 | defaultOptions: { queries: { retry: false } },
7 | })
8 |
9 | const Wrapper = ({ children }: { children: ReactElement }) => (
10 | {children}
11 | )
12 |
13 | // All the providers you need for tests can go here : Theme, Redux, etc.
14 | const customRender = (
15 | ui: ReactElement,
16 | options?: Omit,
17 | ) => render(ui, { wrapper: Wrapper, ...options })
18 |
19 | export * from '@testing-library/react'
20 | export { default as userEvent } from '@testing-library/user-event'
21 | export { customRender as render }
22 |
--------------------------------------------------------------------------------
/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": [{ "name": "next" }],
18 | "paths": { "@/*": ["./src/*"] },
19 | "types": ["@testing-library/jest-dom"]
20 | },
21 | "include": [
22 | "**/*.ts",
23 | "**/*.tsx",
24 | "**/*.test.ts",
25 | "**/*.test.tsx",
26 | "types.d.ts",
27 | "next-env.d.ts",
28 | ".next/types/**/*.ts"
29 | ],
30 | "exclude": ["node_modules", ".next", "out"]
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/api/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import { cookies } from 'next/headers'
3 | import { createServerClient } from '@/utils/supabase'
4 |
5 | export async function GET(request: Request) {
6 | // The `/auth/callback` route is required for the server-side auth flow implemented
7 | // by the Auth Helpers package. It exchanges an auth code for the user's session.
8 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
9 | const requestUrl = new URL(request.url)
10 | const code = requestUrl.searchParams.get('code')
11 |
12 | if (code) {
13 | const cookieStore = cookies()
14 | const supabase = createServerClient(cookieStore)
15 | await supabase.auth.exchangeCodeForSession(code)
16 | }
17 |
18 | // URL to redirect to after sign in process completes
19 | return NextResponse.redirect(requestUrl.origin)
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yaml:
--------------------------------------------------------------------------------
1 | name: Frontend Pull Request Workflow
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | frontend-pull-request:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v4
14 |
15 | - name: Install pnpm
16 | uses: pnpm/action-setup@v4
17 | with:
18 | package_json_file: package.json
19 | run_install: false
20 |
21 | - name: Install Node.js
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 | cache: pnpm
26 |
27 | - name: Install dependencies
28 | run: pnpm i
29 |
30 | - name: Run type check
31 | run: pnpm type-check
32 |
33 | - name: Check linting
34 | run: pnpm lint
35 |
36 | - name: Run format check
37 | run: pnpm format-check
38 |
39 | - name: Run tests
40 | run: pnpm test:ci
41 |
--------------------------------------------------------------------------------
/src/components/ReactQueryExample.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@/test/test-utils'
2 | import ReactQueryExample from './ReactQueryExample'
3 | import { server } from '@/mocks/server'
4 | import { HttpResponse, http } from 'msw'
5 |
6 | describe(' ', () => {
7 | it('Renders the loading screen', () => {
8 | render( )
9 | expect(screen.getByText('Loading...')).toBeInTheDocument()
10 | })
11 |
12 | it('Renders data from the handler', async () => {
13 | render( )
14 |
15 | await screen.findByText('Hello from the handler!')
16 | })
17 |
18 | it('Renders data from overridden handler', async () => {
19 | server.use(
20 | http.get('/api/message', () =>
21 | HttpResponse.json({ message: 'Hello from the overridden handler!' }),
22 | ),
23 | )
24 |
25 | render( )
26 |
27 | await screen.findByText('Hello from the overridden handler!')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/jest.polyfills.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @note The block below contains polyfills for Node.js globals
3 | * required for Jest to function when running JSDOM tests.
4 | * These HAVE to be require's and HAVE to be in this exact
5 | * order, since "undici" depends on the "TextEncoder" global API.
6 | *
7 | * Consider migrating to a more modern test runner if
8 | * you don't want to deal with this.
9 | */
10 |
11 | const { TextDecoder, TextEncoder, ReadableStream } = require('node:util')
12 |
13 | Object.defineProperties(globalThis, {
14 | TextDecoder: { value: TextDecoder },
15 | TextEncoder: { value: TextEncoder },
16 | ReadableStream: { value: ReadableStream },
17 | })
18 |
19 | const { Blob, File } = require('node:buffer')
20 | const { fetch, Headers, FormData, Request, Response } = require('undici')
21 |
22 | Object.defineProperties(globalThis, {
23 | fetch: { value: fetch, writable: true },
24 | Blob: { value: Blob },
25 | File: { value: File },
26 | Headers: { value: Headers },
27 | FormData: { value: FormData },
28 | Request: { value: Request },
29 | Response: { value: Response },
30 | })
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Michael
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/AuthButton.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { cookies } from 'next/headers'
3 | import { redirect } from 'next/navigation'
4 | import { createServerClient } from '@/utils/supabase'
5 |
6 | export default async function AuthButton() {
7 | const cookieStore = cookies()
8 | const supabase = createServerClient(cookieStore)
9 |
10 | const {
11 | data: { user },
12 | } = await supabase.auth.getUser()
13 |
14 | const signOut = async () => {
15 | 'use server'
16 |
17 | const cookieStore = cookies()
18 | const supabase = createServerClient(cookieStore)
19 | await supabase.auth.signOut()
20 | return redirect('/login')
21 | }
22 |
23 | return user ? (
24 |
25 | Hey, {user.email}!
26 |
31 |
32 | ) : (
33 |
37 | Login
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse, type NextRequest } from 'next/server'
2 | import { createMiddlewareClient } from '@/utils/supabase'
3 |
4 | export async function middleware(request: NextRequest) {
5 | try {
6 | // This `try/catch` block is only here for the interactive tutorial.
7 | // Feel free to remove once you have Supabase connected.
8 | const { supabase, response } = createMiddlewareClient(request)
9 |
10 | // Refresh session if expired - required for Server Components
11 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware
12 | await supabase.auth.getSession()
13 |
14 | return response
15 | } catch (e) {
16 | // If you are here, a Supabase client could not be created!
17 | // This is likely because you have not set up environment variables.
18 | // Check out http://localhost:3000 for Next Steps.
19 | return NextResponse.next({
20 | request: { headers: request.headers },
21 | })
22 | }
23 | }
24 |
25 | export const config = {
26 | matcher: [
27 | /*
28 | * Match all request paths except for the ones starting with:
29 | * - _next/static (static files)
30 | * - _next/image (image optimization files)
31 | * - favicon.ico (favicon file)
32 | * Feel free to modify this pattern to include more paths.
33 | */
34 | '/((?!_next/static|_next/image|favicon.ico).*)',
35 | ],
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Code.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 |
5 | const CopyIcon = () => (
6 |
17 |
18 |
19 |
20 | )
21 |
22 | const CheckIcon = () => (
23 |
34 |
35 |
36 | )
37 |
38 | export default function Code({ code }: { code: string }) {
39 | const [icon, setIcon] = useState(CopyIcon)
40 |
41 | const copy = async () => {
42 | await navigator?.clipboard?.writeText(code)
43 | setIcon(CheckIcon)
44 | setTimeout(() => setIcon(CopyIcon), 2000)
45 | }
46 |
47 | return (
48 |
49 |
53 | {icon}
54 |
55 | {code}
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { MoonIcon, SunIcon } from '@radix-ui/react-icons'
4 | import { useTheme } from 'next-themes'
5 | import { Button } from '@/components/ui/button'
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from '@/components/ui/dropdown-menu'
12 |
13 | type ThemeToggleProps = {
14 | side?: 'left' | 'top' | 'right' | 'bottom'
15 | }
16 |
17 | const ThemeToggle = ({ side }: ThemeToggleProps) => {
18 | const { setTheme } = useTheme()
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | Toggle theme
27 |
28 |
29 |
30 | setTheme('light')}>
31 | Light
32 |
33 | setTheme('dark')}>
34 | Dark
35 |
36 | setTheme('system')}>
37 | System
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default ThemeToggle
45 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import NextLogo from './NextLogo'
2 | import SupabaseLogo from './SupabaseLogo'
3 |
4 | export default function Header() {
5 | return (
6 |
7 |
20 |
Supabase and Next.js Starter Template
21 |
22 | The fastest way to build apps with{' '}
23 |
29 | Supabase
30 | {' '}
31 | and{' '}
32 |
38 | Next.js
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5.9% 10%;
26 | --radius: 0.3rem;
27 | }
28 |
29 | .dark {
30 | --background: 240 10% 3.9%;
31 | --foreground: 0 0% 98%;
32 | --card: 240 10% 3.9%;
33 | --card-foreground: 0 0% 98%;
34 | --popover: 240 10% 3.9%;
35 | --popover-foreground: 0 0% 98%;
36 | --primary: 0 0% 98%;
37 | --primary-foreground: 240 5.9% 10%;
38 | --secondary: 240 3.7% 15.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 240 3.7% 15.9%;
41 | --muted-foreground: 240 5% 64.9%;
42 | --accent: 240 3.7% 15.9%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 0% 98%;
46 | --border: 240 3.7% 15.9%;
47 | --input: 240 3.7% 15.9%;
48 | --ring: 240 4.9% 83.9%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 | body {
57 | @apply bg-background text-foreground;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { GeistSans } from 'geist/font/sans'
2 | import ThemeProvider from '@/providers/ThemeProvider'
3 | import NextTopLoader from 'nextjs-toploader'
4 | import { Analytics } from '@vercel/analytics/react'
5 | import './globals.css'
6 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
7 | import ReactQueryProvider from '@/providers/ReactQueryProvider'
8 |
9 | const defaultUrl = process.env.VERCEL_URL
10 | ? `https://${process.env.VERCEL_URL}`
11 | : 'http://localhost:3000'
12 |
13 | export const metadata = {
14 | metadataBase: new URL(defaultUrl),
15 | title: 'Next.js and Supabase Starter Kit',
16 | description: 'The fastest way to build apps with Next.js and Supabase',
17 | }
18 |
19 | export default function RootLayout({
20 | children,
21 | }: {
22 | children: React.ReactNode
23 | }) {
24 | return (
25 |
31 |
32 |
33 |
39 |
40 |
41 | {children}
42 | {' '}
43 | {/* ^^ remove this if you are not deploying to vercel. See more at https://vercel.com/docs/analytics */}
44 |
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/utils/tailwind'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | },
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button'
45 | return (
46 |
51 | )
52 | },
53 | )
54 | Button.displayName = 'Button'
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import AuthButton from '@/components/AuthButton'
2 | import ConnectSupabaseSteps from '@/components/ConnectSupabaseSteps'
3 | import SignUpUserSteps from '@/components/SignUpUserSteps'
4 | import Header from '@/components/Header'
5 | import { cookies } from 'next/headers'
6 | import { createServerClient } from '@/utils/supabase'
7 | import ThemeToggle from '@/components/ThemeToggle'
8 |
9 | export default async function Index() {
10 | const cookieStore = cookies()
11 |
12 | const canInitSupabaseClient = () => {
13 | // This function is just for the interactive tutorial.
14 | // Feel free to remove it once you have Supabase connected.
15 | try {
16 | createServerClient(cookieStore)
17 | return true
18 | } catch (e) {
19 | return false
20 | }
21 | }
22 |
23 | const isSupabaseConnected = canInitSupabaseClient()
24 |
25 | return (
26 |
27 |
28 |
29 | {isSupabaseConnected &&
}
30 |
31 |
32 |
33 |
34 |
35 |
36 | Next steps
37 | {isSupabaseConnected ? : }
38 |
39 |
40 |
41 |
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ConnectSupabaseSteps.tsx:
--------------------------------------------------------------------------------
1 | import Step from './Step'
2 |
3 | export default function ConnectSupabaseSteps() {
4 | return (
5 |
6 |
7 |
8 | Head over to{' '}
9 |
15 | database.new
16 | {' '}
17 | and create a new Supabase project.
18 |
19 |
20 |
21 |
22 |
23 | Rename the{' '}
24 |
25 | .env.example
26 | {' '}
27 | file in your Next.js app to{' '}
28 |
29 | .env.local
30 | {' '}
31 | and populate with values from{' '}
32 |
38 | your Supabase project's API Settings
39 |
40 | .
41 |
42 |
43 |
44 |
45 |
46 | You may need to quit your Next.js development server and run{' '}
47 |
48 | npm run dev
49 | {' '}
50 | again to load the new environment variables.
51 |
52 |
53 |
54 |
55 |
56 | You may need to refresh the page for Next.js to load the new
57 | environment variables.
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: ['./src/**/*.{ts,tsx}'],
5 | prefix: '',
6 | theme: {
7 | container: {
8 | center: true,
9 | padding: '2rem',
10 | screens: { '2xl': '1400px' },
11 | },
12 | extend: {
13 | colors: {
14 | border: 'hsl(var(--border))',
15 | input: 'hsl(var(--input))',
16 | ring: 'hsl(var(--ring))',
17 | background: 'hsl(var(--background))',
18 | foreground: 'hsl(var(--foreground))',
19 | primary: {
20 | DEFAULT: 'hsl(var(--primary))',
21 | foreground: 'hsl(var(--primary-foreground))',
22 | },
23 | secondary: {
24 | DEFAULT: 'hsl(var(--secondary))',
25 | foreground: 'hsl(var(--secondary-foreground))',
26 | },
27 | destructive: {
28 | DEFAULT: 'hsl(var(--destructive))',
29 | foreground: 'hsl(var(--destructive-foreground))',
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))',
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))',
38 | },
39 | popover: {
40 | DEFAULT: 'hsl(var(--popover))',
41 | foreground: 'hsl(var(--popover-foreground))',
42 | },
43 | card: {
44 | DEFAULT: 'hsl(var(--card))',
45 | foreground: 'hsl(var(--card-foreground))',
46 | },
47 | },
48 | borderRadius: {
49 | lg: 'var(--radius)',
50 | md: 'calc(var(--radius) - 2px)',
51 | sm: 'calc(var(--radius) - 4px)',
52 | },
53 | keyframes: {
54 | 'accordion-down': {
55 | from: { height: '0' },
56 | to: { height: 'var(--radix-accordion-content-height)' },
57 | },
58 | 'accordion-up': {
59 | from: { height: 'var(--radix-accordion-content-height)' },
60 | to: { height: '0' },
61 | },
62 | },
63 | animation: {
64 | 'accordion-down': 'accordion-down 0.2s ease-out',
65 | 'accordion-up': 'accordion-up 0.2s ease-out',
66 | },
67 | },
68 | },
69 | plugins: [require('tailwindcss-animate')],
70 | }
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supa-next-starter",
3 | "description": "A production ready Next.js/Supabase starter. ",
4 | "version": "1.0.0",
5 | "private": true,
6 | "author": "Michael Troya (@michaeltroya)",
7 | "license": "MIT",
8 | "keywords": [
9 | "nextjs",
10 | "starter",
11 | "supabase",
12 | "tailwindcss",
13 | "shadcn",
14 | "typescript"
15 | ],
16 | "scripts": {
17 | "dev": "next dev",
18 | "build": "next build",
19 | "start": "next start",
20 | "lint": "next lint",
21 | "format": "prettier --write .",
22 | "format-check": "prettier --check .",
23 | "type-check": "tsc --pretty --noEmit",
24 | "prepare": "husky install",
25 | "test": "jest",
26 | "test:ci": "jest --ci",
27 | "analyze": "ANALYZE=true pnpm build"
28 | },
29 | "dependencies": {
30 | "@next/bundle-analyzer": "^14.0.4",
31 | "@radix-ui/react-dropdown-menu": "^2.0.6",
32 | "@radix-ui/react-icons": "^1.3.0",
33 | "@radix-ui/react-slot": "^1.0.2",
34 | "@supabase/ssr": "^0.1.0",
35 | "@supabase/supabase-js": "^2.39.3",
36 | "@tanstack/react-query": "^5.17.15",
37 | "@tanstack/react-query-devtools": "^5.17.15",
38 | "@vercel/analytics": "^1.1.1",
39 | "axios": "^1.6.5",
40 | "class-variance-authority": "^0.7.0",
41 | "clsx": "^2.1.0",
42 | "geist": "^1.0.0",
43 | "lucide-react": "^0.304.0",
44 | "next": "^14.1.0",
45 | "next-themes": "^0.2.1",
46 | "nextjs-toploader": "^1.6.4",
47 | "react": "18.2.0",
48 | "react-dom": "18.2.0",
49 | "tailwind-merge": "^2.2.0",
50 | "undici": "^5.28.2"
51 | },
52 | "devDependencies": {
53 | "@swc/core": "^1.3.102",
54 | "@swc/jest": "^0.2.29",
55 | "@testing-library/dom": "^9.3.3",
56 | "@testing-library/jest-dom": "^6.2.0",
57 | "@testing-library/react": "^14.1.2",
58 | "@testing-library/user-event": "^14.5.2",
59 | "@types/jest": "^29.5.11",
60 | "@types/node": "20.3.1",
61 | "@types/react": "18.2.8",
62 | "@types/react-dom": "18.2.5",
63 | "autoprefixer": "10.4.15",
64 | "encoding": "^0.1.13",
65 | "eslint": "8.56.0",
66 | "eslint-config-next": "14.0.4",
67 | "husky": "^8.0.0",
68 | "jest": "^29.7.0",
69 | "jest-environment-jsdom": "^29.7.0",
70 | "lint-staged": "^15.2.0",
71 | "msw": "^2.1.1",
72 | "postcss": "8.4.29",
73 | "prettier": "^3.1.1",
74 | "prettier-plugin-tailwindcss": "^0.5.10",
75 | "tailwindcss": "3.3.3",
76 | "tailwindcss-animate": "^1.0.7",
77 | "typescript": "5.1.3"
78 | },
79 | "msw": {
80 | "workerDirectory": [
81 | "public"
82 | ]
83 | },
84 | "packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
85 | }
86 |
--------------------------------------------------------------------------------
/src/utils/supabase.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createBrowserClient as browserClient,
3 | createServerClient as serverClient,
4 | type CookieOptions,
5 | } from '@supabase/ssr'
6 | import { cookies } from 'next/headers'
7 | import { NextRequest, NextResponse } from 'next/server'
8 |
9 | export const createBrowserClient = () =>
10 | browserClient(
11 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
12 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
13 | )
14 |
15 | export const createServerClient = (cookieStore: ReturnType) =>
16 | serverClient(
17 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
18 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
19 | {
20 | cookies: {
21 | get(name: string) {
22 | return cookieStore.get(name)?.value
23 | },
24 | set(name: string, value: string, options: CookieOptions) {
25 | try {
26 | cookieStore.set({ name, value, ...options })
27 | } catch (error) {
28 | // The `set` method was called from a Server Component.
29 | // This can be ignored if you have middleware refreshing
30 | // user sessions.
31 | }
32 | },
33 | remove(name: string, options: CookieOptions) {
34 | try {
35 | cookieStore.set({ name, value: '', ...options })
36 | } catch (error) {
37 | // The `delete` method was called from a Server Component.
38 | // This can be ignored if you have middleware refreshing
39 | // user sessions.
40 | }
41 | },
42 | },
43 | },
44 | )
45 |
46 | export const createMiddlewareClient = (request: NextRequest) => {
47 | // Create an unmodified response
48 | let response = NextResponse.next({ request: { headers: request.headers } })
49 |
50 | const supabase = serverClient(
51 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
52 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
53 | {
54 | cookies: {
55 | get(name: string) {
56 | return request.cookies.get(name)?.value
57 | },
58 | set(name: string, value: string, options: CookieOptions) {
59 | // If the cookie is updated, update the cookies for the request and response
60 | request.cookies.set({ name, value, ...options })
61 | response = NextResponse.next({
62 | request: { headers: request.headers },
63 | })
64 | response.cookies.set({ name, value, ...options })
65 | },
66 | remove(name: string, options: CookieOptions) {
67 | // If the cookie is removed, update the cookies for the request and response
68 | request.cookies.set({ name, value: '', ...options })
69 | response = NextResponse.next({
70 | request: { headers: request.headers },
71 | })
72 | response.cookies.set({ name, value: '', ...options })
73 | },
74 | },
75 | },
76 | )
77 |
78 | return { supabase, response }
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/SignUpUserSteps.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import Step from './Step'
3 | import Code from '@/components/Code'
4 |
5 | const create = `
6 | create table notes (
7 | id serial primary key,
8 | title text
9 | );
10 |
11 | insert into notes(title)
12 | values
13 | ('Today I created a Supabase project.'),
14 | ('I added some data and queried it from Next.js.'),
15 | ('It was awesome!');
16 | `.trim()
17 |
18 | const server = `
19 | import { createServerClient } from '@/utils/supabase'
20 | import { cookies } from 'next/headers'
21 |
22 | export default async function Page() {
23 | const cookieStore = cookies()
24 | const supabase = createServerClient(cookieStore)
25 | const { data: notes } = await supabase.from('notes').select()
26 |
27 | return {JSON.stringify(notes, null, 2)}
28 | }
29 | `.trim()
30 |
31 | const client = `
32 | 'use client'
33 |
34 | import { createBrowserClient } from '@/utils/supabase'
35 | import { useEffect, useState } from 'react'
36 |
37 | export default function Page() {
38 | const [notes, setNotes] = useState(null)
39 | const supabase = createBrowserClient()
40 |
41 | useEffect(() => {
42 | const getData = async () => {
43 | const { data } = await supabase.from('notes').select()
44 | setNotes(data)
45 | }
46 | getData()
47 | }, [])
48 |
49 | return {JSON.stringify(notes, null, 2)}
50 | }
51 | `.trim()
52 |
53 | export default function SignUpUserSteps() {
54 | return (
55 |
56 |
57 |
58 | Head over to the{' '}
59 |
63 | Login
64 | {' '}
65 | page and sign up your first user. It's okay if this is just you
66 | for now. Your awesome idea will have plenty of users later!
67 |
68 |
69 |
70 |
71 |
72 | Head over to the{' '}
73 |
79 | Table Editor
80 | {' '}
81 | for your Supabase project to create a table and insert some example
82 | data. If you're stuck for creativity, you can copy and paste the
83 | following into the{' '}
84 |
90 | SQL Editor
91 | {' '}
92 | and click RUN!
93 |
94 |
95 |
96 |
97 |
98 |
99 | To create a Supabase client and query data from an Async Server
100 | Component, create a new page.tsx file at{' '}
101 |
102 | /app/notes/page.tsx
103 | {' '}
104 | and add the following.
105 |
106 |
107 | Alternatively, you can use a Client Component.
108 |
109 |
110 |
111 |
112 | You're ready to launch your product to the world! 🚀
113 |
114 |
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { headers, cookies } from 'next/headers'
3 | import { redirect } from 'next/navigation'
4 | import { createServerClient } from '@/utils/supabase'
5 |
6 | export default function Login({
7 | searchParams,
8 | }: {
9 | searchParams: { message: string }
10 | }) {
11 | const signIn = async (formData: FormData) => {
12 | 'use server'
13 |
14 | const email = formData.get('email') as string
15 | const password = formData.get('password') as string
16 | const cookieStore = cookies()
17 | const supabase = createServerClient(cookieStore)
18 |
19 | const { error } = await supabase.auth.signInWithPassword({
20 | email,
21 | password,
22 | })
23 |
24 | if (error) {
25 | return redirect('/login?message=Could not authenticate user')
26 | }
27 |
28 | return redirect('/')
29 | }
30 |
31 | const signUp = async (formData: FormData) => {
32 | 'use server'
33 |
34 | const origin = headers().get('origin')
35 | const email = formData.get('email') as string
36 | const password = formData.get('password') as string
37 | const cookieStore = cookies()
38 | const supabase = createServerClient(cookieStore)
39 |
40 | const { error } = await supabase.auth.signUp({
41 | email,
42 | password,
43 | options: {
44 | emailRedirectTo: `${origin}/api/auth/callback`,
45 | },
46 | })
47 |
48 | if (error) {
49 | return redirect('/login?message=Could not authenticate user')
50 | }
51 |
52 | return redirect('/login?message=Check email to continue sign in process')
53 | }
54 |
55 | return (
56 |
57 |
61 |
73 |
74 | {' '}
75 | Back
76 |
77 |
78 |
116 |
117 | )
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/NextLogo.tsx:
--------------------------------------------------------------------------------
1 | export default function NextLogo() {
2 | return (
3 |
10 |
14 |
18 |
22 |
26 |
32 |
36 |
40 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ⚡ SupaNext Starter Kit ⚡
4 |
5 |
6 | The Last Next.js and Supabase Starter You Will Ever Need
7 |
8 |
9 |
17 |
18 |
21 |
22 |
23 |
24 |
25 | Features ·
26 | Clone and run locally ·
27 | Documentation ·
28 | Feedback and issues
29 |
30 |
31 |
32 | ## Features
33 |
34 | - ⚡️ Next.js 14 (App Router)
35 | - 💚 Supabase w/ supabase-ssr - Works across the entire [Next.js](https://nextjs.org) stack (App Router, Pages Router, Client, Server, Middleware, It just works!)
36 | - ⚛️ React 18
37 | - ⛑ TypeScript
38 | - 📦 [pnpm](https://pnpm.io/) - Fast, disk space efficient package manager
39 | - 🎨 [Tailwind](https://tailwindcss.com/)
40 | - 🔌 [shadcn/ui](https://ui.shadcn.com/) - Beautifully designed components that you can copy and paste into your apps.
41 | - 🧪 Jest w/SWC + React Testing Library - Unit tests for all of your code.
42 | - 🎛️ [MSW](https://mswjs.io/)v2 - Intercept requests inside your tests (set up for testing only)
43 | - 🪝[TanStackQuery](https://tanstack.com/query/v5)v5 - The best way to fetch data on the client
44 | - 📏 ESLint — To find and fix problems in your code
45 | - 💖 Prettier — Code Formatter for consistent style
46 | - 🐶 Husky — For running scripts before committing
47 | - 🚫 lint-staged — Run ESLint and Prettier against staged Git files
48 | - 👷 Github Actions — Run Type Checks, Tests, and Linters on Pull Requests
49 | - 🗂 Path Mapping — Import components or images using the `@` prefix
50 | - ⚪⚫ Dark mode - Toggle theme modes with [next-themes](https://github.com/pacocoursey/next-themes)
51 | - ✨ Next Top Loader - Render a pleasent top loader on navigation with [nextjs-toploader](https://github.com/TheSGJ/nextjs-toploader)
52 | - 🔋 Lots Extras - Next Bundle Analyzer, Vercel Analytics, Vercel Geist Font
53 |
54 | ## Clone and run locally
55 |
56 | 1. You'll first need a Supabase project which can be made [via the Supabase dashboard](https://database.new)
57 |
58 | 2. Create a Next.js app using the Supabase Starter template npx command
59 |
60 | ```bash
61 | pnpm create next-app -e https://github.com/michaeltroya/supa-next-starter
62 | # or
63 | npx create-next-app -e https://github.com/michaeltroya/supa-next-starter
64 | ```
65 |
66 | 3. Use `cd` to change into the app's directory
67 |
68 | ```bash
69 | cd name-of-new-app
70 | ```
71 |
72 | 4. Rename `.env.local.example` to `.env.local` and update the following:
73 |
74 | ```
75 | NEXT_PUBLIC_SUPABASE_URL=[INSERT SUPABASE PROJECT URL]
76 | NEXT_PUBLIC_SUPABASE_ANON_KEY=[INSERT SUPABASE PROJECT API ANON KEY]
77 | ```
78 |
79 | Both `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` can be found in [your Supabase project's API settings](https://app.supabase.com/project/_/settings/api)
80 |
81 | 5. You can now run the Next.js local development server:
82 |
83 | ```bash
84 | pnpm run dev
85 | ```
86 |
87 | The starter kit should now be running on [localhost:3000](http://localhost:3000/).
88 |
89 | > Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally.
90 |
91 | ## Showcase
92 |
93 | Websites started using this template:
94 |
95 | - [mainspring.pro](https://www.mainspring.pro/)
96 | - [Add yours](https://github.com/michaeltroya/supa-next-starter/edit/main/README.md)
97 |
98 | # Documentation
99 |
100 | ### Requirements
101 |
102 | - Node.js >= 18.17.0
103 | - pnpm 8
104 |
105 | ### Scripts
106 |
107 | - `pnpm dev` — Starts the application in development mode at `http://localhost:3000`.
108 | - `pnpm build` — Creates an optimized production build of your application.
109 | - `pnpm start` — Starts the application in production mode.
110 | - `pnpm type-check` — Validate code using TypeScript compiler.
111 | - `pnpm lint` — Runs ESLint for all files in the `src` directory.
112 | - `pnpm format-check` — Runs Prettier and checks if any files have formatting issues.
113 | - `pnpm format` — Runs Prettier and formats files.
114 | - `pnpm test` — Runs all the jest tests in the project.
115 | - `pnpm test:ci` — Runs all the jest tests in the project, Jest will assume it is running in a CI environment.
116 | - `pnpm analyze` — Builds the project and opens the bundle analyzer.
117 |
118 | ### Paths
119 |
120 | TypeScript is pre-configured with custom path mappings. To import components or files, use the `@` prefix.
121 |
122 | ```tsx
123 | import { Button } from '@/components/ui/Button'
124 |
125 | // To import images or other files from the public folder
126 | import avatar from '@/public/avatar.png'
127 | ```
128 |
129 | ### Switch to Yarn/npm
130 |
131 | This starter uses pnpm by default, but this choice is yours. If you'd like to switch to Yarn/npm, delete the `pnpm-lock.yaml` file, install the dependencies with Yarn/npm, change the CI workflow, and Husky Git hooks to use Yarn/npm commands.
132 |
133 | ## License
134 |
135 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for more information.
136 |
137 | ## Feedback and issues
138 |
139 | Please file feedback and issues [here](https://github.com/michaeltroya/supa-next-starter/issues).
140 |
--------------------------------------------------------------------------------
/src/components/SupabaseLogo.tsx:
--------------------------------------------------------------------------------
1 | export default function SupabaseLogo() {
2 | return (
3 |
11 |
12 |
13 |
17 |
22 |
26 |
27 |
31 |
35 |
39 |
43 |
47 |
51 |
55 |
59 |
60 |
61 |
69 |
70 |
71 |
72 |
80 |
81 |
82 |
83 |
84 |
90 |
91 |
92 |
98 |
99 |
100 |
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from '@radix-ui/react-icons'
6 |
7 | import { cn } from '@/utils/tailwind'
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------